Merge branch 'feature-privacy-federation' into development
This commit is contained in:
		
						commit
						e6408b2e7a
					
				
					 59 changed files with 451 additions and 208 deletions
				
			
		| 
						 | 
				
			
			@ -40,10 +40,11 @@ const ColumnCollapsable = React.createClass({
 | 
			
		|||
  render () {
 | 
			
		||||
    const { icon, fullHeight, children } = this.props;
 | 
			
		||||
    const { collapsed } = this.state;
 | 
			
		||||
 | 
			
		||||
    const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable';
 | 
			
		||||
    
 | 
			
		||||
    return (
 | 
			
		||||
      <div style={{ position: 'relative' }}>
 | 
			
		||||
        <div style={{...iconStyle, color: collapsed ? '#9baec8' : '#fff', background: collapsed ? '#2f3441' : '#373b4a' }} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
 | 
			
		||||
        <div style={{...iconStyle }} className={collapsedClassName} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
 | 
			
		||||
 | 
			
		||||
        <Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
 | 
			
		||||
          {({ opacity, height }) =>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,12 +4,11 @@ const style = {
 | 
			
		|||
  textAlign: 'center',
 | 
			
		||||
  fontSize: '16px',
 | 
			
		||||
  fontWeight: '500',
 | 
			
		||||
  color: '#616b86',
 | 
			
		||||
  paddingTop: '120px'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const LoadingIndicator = () => (
 | 
			
		||||
  <div style={style}>
 | 
			
		||||
  <div className='loading-indicator' style={style}>
 | 
			
		||||
    <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,8 +16,6 @@ const outerStyle = {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
const spoilerStyle = {
 | 
			
		||||
  background: '#000',
 | 
			
		||||
  color: '#fff',
 | 
			
		||||
  textAlign: 'center',
 | 
			
		||||
  height: '100%',
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
| 
						 | 
				
			
			@ -84,14 +82,14 @@ const MediaGallery = React.createClass({
 | 
			
		|||
    if (!this.state.visible) {
 | 
			
		||||
      if (sensitive) {
 | 
			
		||||
        children = (
 | 
			
		||||
          <div style={spoilerStyle} onClick={this.handleOpen}>
 | 
			
		||||
          <div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
 | 
			
		||||
            <span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
 | 
			
		||||
            <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        children = (
 | 
			
		||||
          <div style={spoilerStyle} onClick={this.handleOpen}>
 | 
			
		||||
          <div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
 | 
			
		||||
            <span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
 | 
			
		||||
            <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
 | 
			
		||||
          </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,8 +28,6 @@ const muteStyle = {
 | 
			
		|||
 | 
			
		||||
const spoilerStyle = {
 | 
			
		||||
  marginTop: '8px',
 | 
			
		||||
  background: '#000',
 | 
			
		||||
  color: '#fff',
 | 
			
		||||
  textAlign: 'center',
 | 
			
		||||
  height: '100%',
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
| 
						 | 
				
			
			@ -122,7 +120,7 @@ const VideoPlayer = React.createClass({
 | 
			
		|||
    if (!this.state.visible) {
 | 
			
		||||
      if (sensitive) {
 | 
			
		||||
        return (
 | 
			
		||||
          <div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} onClick={this.handleVisibility}>
 | 
			
		||||
          <div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
 | 
			
		||||
            {spoilerButton}
 | 
			
		||||
            <span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
 | 
			
		||||
            <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
 | 
			
		||||
| 
						 | 
				
			
			@ -130,7 +128,7 @@ const VideoPlayer = React.createClass({
 | 
			
		|||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        return (
 | 
			
		||||
          <div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} onClick={this.handleOpen}>
 | 
			
		||||
          <div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleOpen}>
 | 
			
		||||
            {spoilerButton}
 | 
			
		||||
            <span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
 | 
			
		||||
            <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,7 +35,7 @@ const Header = React.createClass({
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
 | 
			
		||||
      info = <span style={{ position: 'absolute', top: '10px', right: '10px', opacity: '0.7', display: 'inline-block', verticalAlign: 'top', background: 'rgba(0, 0, 0, 0.4)', color: '#fff', textTransform: 'uppercase', fontSize: '11px', fontWeight: '500', padding: '4px', borderRadius: '4px' }}><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>
 | 
			
		||||
      info = <span className='account--follows-info' style={{ position: 'absolute', top: '10px', right: '10px', opacity: '0.7', display: 'inline-block', verticalAlign: 'top', background: 'rgba(0, 0, 0, 0.4)', textTransform: 'uppercase', fontSize: '11px', fontWeight: '500', padding: '4px', borderRadius: '4px' }}><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (me !== account.get('id')) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,11 +16,8 @@ const outerStyle = {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
const panelStyle = {
 | 
			
		||||
  background: '#2f3441',
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexDirection: 'row',
 | 
			
		||||
  borderTop: '1px solid #363c4b',
 | 
			
		||||
  borderBottom: '1px solid #363c4b',
 | 
			
		||||
  padding: '10px 0'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,10 +37,10 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
 | 
			
		|||
          <DisplayName account={account} />
 | 
			
		||||
        </Permalink>
 | 
			
		||||
 | 
			
		||||
        <div style={{ color: '#616b86', fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
 | 
			
		||||
        <div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div style={panelStyle}>
 | 
			
		||||
      <div className='account--panel' style={panelStyle}>
 | 
			
		||||
        <div style={btnStyle}><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div>
 | 
			
		||||
        <div style={btnStyle}><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div>
 | 
			
		||||
      </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,6 @@ const messages = defineMessages({
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
const outerStyle = {
 | 
			
		||||
  background: '#373b4a',
 | 
			
		||||
  padding: '15px'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +17,6 @@ const sectionStyle = {
 | 
			
		|||
  cursor: 'default',
 | 
			
		||||
  display: 'block',
 | 
			
		||||
  fontWeight: '500',
 | 
			
		||||
  color: '#9baec8',
 | 
			
		||||
  marginBottom: '10px'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -42,8 +40,8 @@ const ColumnSettings = React.createClass({
 | 
			
		|||
 | 
			
		||||
    return (
 | 
			
		||||
      <ColumnCollapsable icon='sliders' fullHeight={209} onCollapse={onSave}>
 | 
			
		||||
        <div style={outerStyle}>
 | 
			
		||||
          <span style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
 | 
			
		||||
        <div className='column-settings--outer' style={outerStyle}>
 | 
			
		||||
          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
 | 
			
		||||
 | 
			
		||||
          <div style={rowStyle}>
 | 
			
		||||
            <SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reblogs' />} />
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +51,7 @@ const ColumnSettings = React.createClass({
 | 
			
		|||
            <SettingToggle settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <span style={sectionStyle}><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
 | 
			
		||||
          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
 | 
			
		||||
 | 
			
		||||
          <div style={rowStyle}>
 | 
			
		||||
            <SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,8 +4,7 @@ const iconStyle = {
 | 
			
		|||
  position: 'absolute',
 | 
			
		||||
  right: '48px',
 | 
			
		||||
  top: '0',
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
  background: '#2f3441'
 | 
			
		||||
  cursor: 'pointer'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ClearColumnButton = ({ onClick }) => (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,6 @@ import ColumnCollapsable from '../../../components/column_collapsable';
 | 
			
		|||
import SettingToggle from './setting_toggle';
 | 
			
		||||
 | 
			
		||||
const outerStyle = {
 | 
			
		||||
  background: '#373b4a',
 | 
			
		||||
  padding: '15px'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +12,6 @@ const sectionStyle = {
 | 
			
		|||
  cursor: 'default',
 | 
			
		||||
  display: 'block',
 | 
			
		||||
  fontWeight: '500',
 | 
			
		||||
  color: '#9baec8',
 | 
			
		||||
  marginBottom: '10px'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,8 +38,8 @@ const ColumnSettings = React.createClass({
 | 
			
		|||
 | 
			
		||||
    return (
 | 
			
		||||
      <ColumnCollapsable icon='sliders' fullHeight={616} onCollapse={onSave}>
 | 
			
		||||
        <div style={outerStyle}>
 | 
			
		||||
          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
 | 
			
		||||
        <div className='column-settings--outer' style={outerStyle}>
 | 
			
		||||
          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
 | 
			
		||||
 | 
			
		||||
          <div style={rowStyle}>
 | 
			
		||||
            <SettingToggle settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +47,7 @@ const ColumnSettings = React.createClass({
 | 
			
		|||
            <SettingToggle settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
 | 
			
		||||
          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
 | 
			
		||||
 | 
			
		||||
          <div style={rowStyle}>
 | 
			
		||||
            <SettingToggle settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +55,7 @@ const ColumnSettings = React.createClass({
 | 
			
		|||
            <SettingToggle settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
 | 
			
		||||
          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
 | 
			
		||||
 | 
			
		||||
          <div style={rowStyle}>
 | 
			
		||||
            <SettingToggle settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +63,7 @@ const ColumnSettings = React.createClass({
 | 
			
		|||
            <SettingToggle settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
 | 
			
		||||
          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
 | 
			
		||||
 | 
			
		||||
          <div style={rowStyle}>
 | 
			
		||||
            <SettingToggle settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,16 +7,6 @@ import Permalink from '../../../components/permalink';
 | 
			
		|||
import emojify from '../../../emoji';
 | 
			
		||||
import escapeTextContentForBrowser from 'react/lib/escapeTextContentForBrowser';
 | 
			
		||||
 | 
			
		||||
const messageStyle = {
 | 
			
		||||
  marginLeft: '68px',
 | 
			
		||||
  padding: '8px 0',
 | 
			
		||||
  paddingBottom: '0',
 | 
			
		||||
  cursor: 'default',
 | 
			
		||||
  color: '#d9e1e8',
 | 
			
		||||
  fontSize: '15px',
 | 
			
		||||
  position: 'relative'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const linkStyle = {
 | 
			
		||||
  fontWeight: '500'
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -32,9 +22,9 @@ const Notification = React.createClass({
 | 
			
		|||
  renderFollow (account, link) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='notification'>
 | 
			
		||||
        <div style={messageStyle}>
 | 
			
		||||
        <div className='notification__message'>
 | 
			
		||||
          <div style={{ position: 'absolute', 'left': '-26px'}}>
 | 
			
		||||
            <i className='fa fa-fw fa-user-plus' style={{ color: '#2b90d9' }} />
 | 
			
		||||
            <i className='fa fa-fw fa-user-plus' />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
 | 
			
		||||
| 
						 | 
				
			
			@ -52,7 +42,7 @@ const Notification = React.createClass({
 | 
			
		|||
  renderFavourite (notification, link) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='notification'>
 | 
			
		||||
        <div style={messageStyle}>
 | 
			
		||||
        <div className='notification__message'>
 | 
			
		||||
          <div style={{ position: 'absolute', 'left': '-26px'}}>
 | 
			
		||||
            <i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} />
 | 
			
		||||
          </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -68,9 +58,9 @@ const Notification = React.createClass({
 | 
			
		|||
  renderReblog (notification, link) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='notification'>
 | 
			
		||||
        <div style={messageStyle}>
 | 
			
		||||
        <div className='notification__message'>
 | 
			
		||||
          <div style={{ position: 'absolute', 'left': '-26px'}}>
 | 
			
		||||
            <i className='fa fa-fw fa-retweet' style={{ color: '#2b90d9' }} />
 | 
			
		||||
            <i className='fa fa-fw fa-retweet' />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,14 +11,13 @@ const labelSpanStyle = {
 | 
			
		|||
  display: 'inline-block',
 | 
			
		||||
  verticalAlign: 'middle',
 | 
			
		||||
  marginBottom: '14px',
 | 
			
		||||
  marginLeft: '8px',
 | 
			
		||||
  color: '#9baec8'
 | 
			
		||||
  marginLeft: '8px'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const SettingToggle = ({ settings, settingKey, label, onChange }) => (
 | 
			
		||||
  <label style={labelStyle}>
 | 
			
		||||
    <Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
 | 
			
		||||
    <span style={labelSpanStyle}>{label}</span>
 | 
			
		||||
    <span className='setting-toggle' style={labelSpanStyle}>{label}</span>
 | 
			
		||||
  </label>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,6 @@
 | 
			
		|||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
 | 
			
		||||
const outerStyle = {
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
  fontSize: '14px',
 | 
			
		||||
  border: '1px solid #363c4b',
 | 
			
		||||
  borderRadius: '4px',
 | 
			
		||||
  color: '#616b86',
 | 
			
		||||
  marginTop: '14px',
 | 
			
		||||
  textDecoration: 'none',
 | 
			
		||||
  overflow: 'hidden'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const contentStyle = {
 | 
			
		||||
  flex: '1 1 auto',
 | 
			
		||||
  padding: '8px',
 | 
			
		||||
| 
						 | 
				
			
			@ -20,25 +8,6 @@ const contentStyle = {
 | 
			
		|||
  overflow: 'hidden'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const titleStyle = {
 | 
			
		||||
  display: 'block',
 | 
			
		||||
  fontWeight: '500',
 | 
			
		||||
  marginBottom: '5px',
 | 
			
		||||
  color: '#d9e1e8',
 | 
			
		||||
  overflow: 'hidden',
 | 
			
		||||
  textOverflow: 'ellipsis',
 | 
			
		||||
  whiteSpace: 'nowrap'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const descriptionStyle = {
 | 
			
		||||
  color: '#d9e1e8'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const imageOuterStyle = {
 | 
			
		||||
  flex: '0 0 100px',
 | 
			
		||||
  background: '#373b4a'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const imageStyle = {
 | 
			
		||||
  display: 'block',
 | 
			
		||||
  width: '100%',
 | 
			
		||||
| 
						 | 
				
			
			@ -77,20 +46,20 @@ const Card = React.createClass({
 | 
			
		|||
 | 
			
		||||
    if (card.get('image')) {
 | 
			
		||||
      image = (
 | 
			
		||||
        <div style={imageOuterStyle}>
 | 
			
		||||
        <div className='status-card__image'>
 | 
			
		||||
          <img src={card.get('image')} alt={card.get('title')} style={imageStyle} />
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <a style={outerStyle} href={card.get('url')} className='status-card'>
 | 
			
		||||
      <a href={card.get('url')} className='status-card'>
 | 
			
		||||
        {image}
 | 
			
		||||
 | 
			
		||||
        <div style={contentStyle}>
 | 
			
		||||
          <strong style={titleStyle} title={card.get('title')}>{card.get('title')}</strong>
 | 
			
		||||
          <p style={descriptionStyle}>{card.get('description').substring(0, 50)}</p>
 | 
			
		||||
          <span style={hostStyle}>{getHostname(card.get('url'))}</span>
 | 
			
		||||
        <div className='status-card__content' style={contentStyle}>
 | 
			
		||||
          <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
 | 
			
		||||
          <p className='status-card__description'>{card.get('description').substring(0, 50)}</p>
 | 
			
		||||
          <span className='status-card__host' style={hostStyle}>{getHostname(card.get('url'))}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </a>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -52,7 +52,7 @@ const DetailedStatus = React.createClass({
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div style={{ background: '#2f3441', padding: '14px 10px' }} className='detailed-status'>
 | 
			
		||||
      <div style={{ padding: '14px 10px' }} className='detailed-status'>
 | 
			
		||||
        <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
 | 
			
		||||
          <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} size={48} /></div>
 | 
			
		||||
          <DisplayName account={status.get('account')} />
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +62,7 @@ const DetailedStatus = React.createClass({
 | 
			
		|||
 | 
			
		||||
        {media}
 | 
			
		||||
 | 
			
		||||
        <div style={{ marginTop: '15px', color: '#616b86', fontSize: '14px', lineHeight: '18px' }}>
 | 
			
		||||
        <div className='detailed-status__meta'>
 | 
			
		||||
          <a className='detailed-status__datetime' style={{ color: 'inherit' }} href={status.get('url')} target='_blank' rel='noopener'><FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /></a>{applicationLink} · <Link to={`/statuses/${status.get('id')}/reblogs`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-retweet' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('reblogs_count')} /></span></Link> · <Link to={`/statuses/${status.get('id')}/favourites`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-star' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('favourites_count')} /></span></Link>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,6 @@ const outerStyle = {
 | 
			
		|||
  display: 'block',
 | 
			
		||||
  padding: '15px',
 | 
			
		||||
  fontSize: '16px',
 | 
			
		||||
  color: '#fff',
 | 
			
		||||
  textDecoration: 'none'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,13 +41,12 @@ const imageStyle = {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
const loadingStyle = {
 | 
			
		||||
  background: '#373b4a',
 | 
			
		||||
  width: '400px',
 | 
			
		||||
  paddingBottom: '120px'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const preloader = () => (
 | 
			
		||||
  <div style={loadingStyle}>
 | 
			
		||||
  <div className='modal-container--preloader' style={loadingStyle}>
 | 
			
		||||
    <LoadingIndicator />
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +56,6 @@ const leftNavStyle = {
 | 
			
		|||
  background: 'rgba(0, 0, 0, 0.5)',
 | 
			
		||||
  padding: '30px 15px',
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
  color: '#fff',
 | 
			
		||||
  fontSize: '24px',
 | 
			
		||||
  top: '0',
 | 
			
		||||
  left: '-61px',
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +70,6 @@ const rightNavStyle = {
 | 
			
		|||
  background: 'rgba(0, 0, 0, 0.5)',
 | 
			
		||||
  padding: '30px 15px',
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
  color: '#fff',
 | 
			
		||||
  fontSize: '24px',
 | 
			
		||||
  top: '0',
 | 
			
		||||
  right: '-61px',
 | 
			
		||||
| 
						 | 
				
			
			@ -143,11 +140,11 @@ const Modal = React.createClass({
 | 
			
		|||
    leftNav = rightNav = '';
 | 
			
		||||
 | 
			
		||||
    if (hasLeft) {
 | 
			
		||||
      leftNav = <div style={leftNavStyle} onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
 | 
			
		||||
      leftNav = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (hasRight) {
 | 
			
		||||
      rightNav = <div style={rightNavStyle} onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
 | 
			
		||||
      rightNav = <div style={rightNavStyle} className='modal-container--nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -256,6 +256,35 @@ button:focus {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.compact-header {
 | 
			
		||||
  h1 {
 | 
			
		||||
    font-size: 24px;
 | 
			
		||||
    line-height: 28px;
 | 
			
		||||
    color: $color3;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
 | 
			
		||||
    a {
 | 
			
		||||
      color: inherit;
 | 
			
		||||
      text-decoration: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    small {
 | 
			
		||||
      font-weight: 400;
 | 
			
		||||
      color: $color2;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    img {
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
      margin-bottom: -5px;
 | 
			
		||||
      margin-right: 15px;
 | 
			
		||||
      width: 36px;
 | 
			
		||||
      height: 36px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@import 'forms';
 | 
			
		||||
@import 'accounts';
 | 
			
		||||
@import 'stream_entries';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,6 +34,7 @@
 | 
			
		|||
 | 
			
		||||
.column-icon {
 | 
			
		||||
  color: $color3;
 | 
			
		||||
  background: lighten($color1, 4%);
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: lighten($color3, 7%);
 | 
			
		||||
| 
						 | 
				
			
			@ -187,7 +188,7 @@
 | 
			
		|||
a.status__content__spoiler-link {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  border-radius: 2px;
 | 
			
		||||
  color: lighten($color1, 6%);
 | 
			
		||||
  color: lighten($color1, 8%);
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  font-size: 11px;
 | 
			
		||||
  padding: 0px 6px;
 | 
			
		||||
| 
						 | 
				
			
			@ -200,7 +201,7 @@ a.status__content__spoiler-link {
 | 
			
		|||
  padding-left: 68px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  min-height: 48px;
 | 
			
		||||
  border-bottom: 1px solid lighten($color1, 6%);
 | 
			
		||||
  border-bottom: 1px solid lighten($color1, 8%);
 | 
			
		||||
  cursor: default;
 | 
			
		||||
 | 
			
		||||
  .status__relative-time {
 | 
			
		||||
| 
						 | 
				
			
			@ -226,6 +227,8 @@ a.status__content__spoiler-link {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.detailed-status {
 | 
			
		||||
  background: lighten($color1, 4%);
 | 
			
		||||
 | 
			
		||||
  .status__content {
 | 
			
		||||
    font-size: 19px;
 | 
			
		||||
    line-height: 24px;
 | 
			
		||||
| 
						 | 
				
			
			@ -237,12 +240,19 @@ a.status__content__spoiler-link {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.detailed-status__meta {
 | 
			
		||||
  margin-top: 15px;
 | 
			
		||||
  color: lighten($color1, 26%);
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  line-height: 18px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.detailed-status__action-bar {
 | 
			
		||||
  background: lighten($color1, 4%);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  border-top: 1px solid lighten($color1, 6%);
 | 
			
		||||
  border-bottom: 1px solid lighten($color1, 6%);
 | 
			
		||||
  border-top: 1px solid lighten($color1, 8%);
 | 
			
		||||
  border-bottom: 1px solid lighten($color1, 8%);
 | 
			
		||||
  padding: 10px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -257,7 +267,7 @@ a.status__content__spoiler-link {
 | 
			
		|||
 | 
			
		||||
.account {
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  border-bottom: 1px solid lighten($color1, 6%);
 | 
			
		||||
  border-bottom: 1px solid lighten($color1, 8%);
 | 
			
		||||
 | 
			
		||||
  .account__display-name {
 | 
			
		||||
    flex: 1 1 auto;
 | 
			
		||||
| 
						 | 
				
			
			@ -298,6 +308,7 @@ a.status__content__spoiler-link {
 | 
			
		|||
  word-wrap: break-word;
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  color: $color3;
 | 
			
		||||
 | 
			
		||||
  p {
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
| 
						 | 
				
			
			@ -325,8 +336,8 @@ a.status__content__spoiler-link {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.account__action-bar {
 | 
			
		||||
  border-top: 1px solid lighten($color1, 6%);
 | 
			
		||||
  border-bottom: 1px solid lighten($color1, 6%);
 | 
			
		||||
  border-top: 1px solid lighten($color1, 8%);
 | 
			
		||||
  border-bottom: 1px solid lighten($color1, 8%);
 | 
			
		||||
  line-height: 36px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
| 
						 | 
				
			
			@ -337,7 +348,7 @@ a.status__content__spoiler-link {
 | 
			
		|||
  text-decoration: none;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  width: 80px;
 | 
			
		||||
  border-left: 1px solid lighten($color1, 6%);
 | 
			
		||||
  border-left: 1px solid lighten($color1, 8%);
 | 
			
		||||
  padding: 10px 5px;
 | 
			
		||||
 | 
			
		||||
  & > span {
 | 
			
		||||
| 
						 | 
				
			
			@ -412,8 +423,9 @@ a.status__content__spoiler-link {
 | 
			
		|||
    opacity: 0.5;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .status__content__spoiler-link {
 | 
			
		||||
  a.status__content__spoiler-link {
 | 
			
		||||
    background: lighten($color1, 26%);
 | 
			
		||||
    color: lighten($color1, 4%);
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background: lighten($color1, 29%);
 | 
			
		||||
| 
						 | 
				
			
			@ -422,6 +434,20 @@ a.status__content__spoiler-link {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.notification__message {
 | 
			
		||||
  margin-left: 68px;
 | 
			
		||||
  padding: 8px 0;
 | 
			
		||||
  padding-bottom: 0;
 | 
			
		||||
  cursor: default;
 | 
			
		||||
  color: $color3;
 | 
			
		||||
  font-size: 15px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  .fa {
 | 
			
		||||
    color: $color4;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.notification__display-name {
 | 
			
		||||
  color: inherit;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
| 
						 | 
				
			
			@ -646,7 +672,7 @@ a.status__content__spoiler-link {
 | 
			
		|||
 | 
			
		||||
.tabs-bar {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  background: lighten($color1, 6%);
 | 
			
		||||
  background: lighten($color1, 8%);
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -660,7 +686,7 @@ a.status__content__spoiler-link {
 | 
			
		|||
  text-align: center;
 | 
			
		||||
  font-size:12px;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  border-bottom: 2px solid lighten($color1, 6%);
 | 
			
		||||
  border-bottom: 2px solid lighten($color1, 8%);
 | 
			
		||||
 | 
			
		||||
  &.active {
 | 
			
		||||
    border-bottom: 2px solid $color4;
 | 
			
		||||
| 
						 | 
				
			
			@ -850,7 +876,8 @@ a.status__content__spoiler-link {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.column-link {
 | 
			
		||||
  background: lighten($color1, 6%);
 | 
			
		||||
  background: lighten($color1, 8%);
 | 
			
		||||
  color: $color5;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background: lighten($color1, 11%);
 | 
			
		||||
| 
						 | 
				
			
			@ -883,6 +910,7 @@ a.status__content__spoiler-link {
 | 
			
		|||
 | 
			
		||||
.autosuggest-textarea__textarea {
 | 
			
		||||
  height: 100px;
 | 
			
		||||
  background: $color5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.autosuggest-textarea__suggestions {
 | 
			
		||||
| 
						 | 
				
			
			@ -968,11 +996,40 @@ button.active i.fa-retweet {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.status-card {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  border: 1px solid lighten($color1, 8%);
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  color: lighten($color1, 26%);
 | 
			
		||||
  margin-top: 14px;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background: lighten($color1, 6%);
 | 
			
		||||
    background: lighten($color1, 8%);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status-card__title {
 | 
			
		||||
  display: block;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  margin-bottom: 5px;
 | 
			
		||||
  color: $color3;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status-card__description {
 | 
			
		||||
  color: $color3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status-card__image {
 | 
			
		||||
  flex: 0 0 100px;
 | 
			
		||||
  background: lighten($color1, 8%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.load-more {
 | 
			
		||||
  display: block;
 | 
			
		||||
  color: lighten($color1, 26%);
 | 
			
		||||
| 
						 | 
				
			
			@ -981,7 +1038,7 @@ button.active i.fa-retweet {
 | 
			
		|||
  text-decoration: none;
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background: lighten($color1, 6%);
 | 
			
		||||
    background: lighten($color1, 8%);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1020,3 +1077,53 @@ button.active i.fa-retweet {
 | 
			
		|||
  font-size: 14px;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loading-indicator {
 | 
			
		||||
  color: $color2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.collapsable-collapsed {
 | 
			
		||||
  color: $color3;
 | 
			
		||||
  background: lighten($color1, 4%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.collapsable {
 | 
			
		||||
  color: $color5;
 | 
			
		||||
  background: lighten($color1, 8%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.media-spoiler {
 | 
			
		||||
  background: $color8;
 | 
			
		||||
  color: $color5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-container--preloader {
 | 
			
		||||
  background: lighten($color1, 8%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.account--panel {
 | 
			
		||||
  background: lighten($color1, 4%);
 | 
			
		||||
  border-top: 1px solid lighten($color1, 8%);
 | 
			
		||||
  border-bottom: 1px solid lighten($color1, 8%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.column-settings--outer {
 | 
			
		||||
  background: lighten($color1, 8%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.column-settings--section {
 | 
			
		||||
  color: $color3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-container--nav {
 | 
			
		||||
  color: $color5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.account--follows-info {
 | 
			
		||||
  color: $color5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-toggle {
 | 
			
		||||
  color: $color3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,24 +5,24 @@
 | 
			
		|||
  .entry {
 | 
			
		||||
    background: lighten($color2, 8%);
 | 
			
		||||
 | 
			
		||||
    &, .detailed-status.light {
 | 
			
		||||
    .detailed-status.light, .status.light {
 | 
			
		||||
      border-bottom: 1px solid $color2;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      &, .detailed-status.light {
 | 
			
		||||
      &, .detailed-status.light, .status.light {
 | 
			
		||||
        border-bottom: 0;
 | 
			
		||||
        border-radius: 0 0 4px 4px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:first-child {
 | 
			
		||||
      &, .detailed-status.light {
 | 
			
		||||
      &, .detailed-status.light, .status.light {
 | 
			
		||||
        border-radius: 4px 4px 0 0;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &:last-child {
 | 
			
		||||
        &, .detailed-status.light {
 | 
			
		||||
        &, .detailed-status.light, .status.light {
 | 
			
		||||
          border-radius: 4px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,12 +18,12 @@ class Api::V1::FollowRequestsController < ApiController
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def authorize
 | 
			
		||||
    FollowRequest.find_by!(account_id: params[:id], target_account: current_account).authorize!
 | 
			
		||||
    AuthorizeFollowService.new.call(Account.find(params[:id]), current_account)
 | 
			
		||||
    render_empty
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def reject
 | 
			
		||||
    FollowRequest.find_by!(account_id: params[:id], target_account: current_account).reject!
 | 
			
		||||
    RejectFollowService.new.call(Account.find(params[:id]), current_account)
 | 
			
		||||
    render_empty
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module ObfuscateFilename
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -143,6 +143,10 @@ module AtomBuilderHelper
 | 
			
		|||
    xml.link(:rel => 'mentioned', :href => TagManager::COLLECTIONS[:public], 'ostatus:object-type' => TagManager::TYPES[:collection])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def privacy_scope(xml, level)
 | 
			
		||||
    xml['mastodon'].scope(level)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def include_author(xml, account)
 | 
			
		||||
    object_type      xml, :person
 | 
			
		||||
    uri              xml, TagManager.instance.uri_for(account)
 | 
			
		||||
| 
						 | 
				
			
			@ -152,6 +156,7 @@ module AtomBuilderHelper
 | 
			
		|||
    link_alternate   xml, TagManager.instance.url_for(account)
 | 
			
		||||
    link_avatar      xml, account
 | 
			
		||||
    portable_contact xml, account
 | 
			
		||||
    privacy_scope    xml, account.locked? ? :private : :public
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def rich_content(xml, activity)
 | 
			
		||||
| 
						 | 
				
			
			@ -216,6 +221,7 @@ module AtomBuilderHelper
 | 
			
		|||
          end
 | 
			
		||||
 | 
			
		||||
          category(xml, 'nsfw') if stream_entry.target.sensitive?
 | 
			
		||||
          privacy_scope(xml, stream_entry.target.visibility)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -237,6 +243,7 @@ module AtomBuilderHelper
 | 
			
		|||
    end
 | 
			
		||||
 | 
			
		||||
    category(xml, 'nsfw') if stream_entry.activity.sensitive?
 | 
			
		||||
    privacy_scope(xml, stream_entry.activity.visibility)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
| 
						 | 
				
			
			@ -249,6 +256,7 @@ module AtomBuilderHelper
 | 
			
		|||
               'xmlns:poco'     => TagManager::POCO_XMLNS,
 | 
			
		||||
               'xmlns:media'    => TagManager::MEDIA_XMLNS,
 | 
			
		||||
               'xmlns:ostatus'  => TagManager::OS_XMLNS,
 | 
			
		||||
               'xmlns:mastodon' => TagManager::MTDN_XMLNS,
 | 
			
		||||
             }, &block)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -107,7 +107,6 @@ class FeedManager
 | 
			
		|||
    should_filter ||= receiver.blocking?(status.account)                                    # or it's from someone I blocked
 | 
			
		||||
    should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked
 | 
			
		||||
    should_filter ||= (status.account.silenced? && !receiver.following?(status.account))    # of if the account is silenced and I'm not following them
 | 
			
		||||
    should_filter ||= (status.private_visibility? && !receiver.following?(status.account))  # or if the mentioned account is not permitted to see the private status
 | 
			
		||||
 | 
			
		||||
    if status.reply? && !status.in_reply_to_account_id.nil?                                 # or it's a reply
 | 
			
		||||
      should_filter ||= receiver.blocking?(status.in_reply_to_account)                      # to a user I blocked
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,15 +7,18 @@ class TagManager
 | 
			
		|||
  include RoutingHelper
 | 
			
		||||
 | 
			
		||||
  VERBS = {
 | 
			
		||||
    post:       'http://activitystrea.ms/schema/1.0/post',
 | 
			
		||||
    share:      'http://activitystrea.ms/schema/1.0/share',
 | 
			
		||||
    favorite:   'http://activitystrea.ms/schema/1.0/favorite',
 | 
			
		||||
    unfavorite: 'http://activitystrea.ms/schema/1.0/unfavorite',
 | 
			
		||||
    delete:     'http://activitystrea.ms/schema/1.0/delete',
 | 
			
		||||
    follow:     'http://activitystrea.ms/schema/1.0/follow',
 | 
			
		||||
    unfollow:   'http://ostatus.org/schema/1.0/unfollow',
 | 
			
		||||
    block:      'http://mastodon.social/schema/1.0/block',
 | 
			
		||||
    unblock:    'http://mastodon.social/schema/1.0/unblock',
 | 
			
		||||
    post:           'http://activitystrea.ms/schema/1.0/post',
 | 
			
		||||
    share:          'http://activitystrea.ms/schema/1.0/share',
 | 
			
		||||
    favorite:       'http://activitystrea.ms/schema/1.0/favorite',
 | 
			
		||||
    unfavorite:     'http://activitystrea.ms/schema/1.0/unfavorite',
 | 
			
		||||
    delete:         'http://activitystrea.ms/schema/1.0/delete',
 | 
			
		||||
    follow:         'http://activitystrea.ms/schema/1.0/follow',
 | 
			
		||||
    request_friend: 'http://activitystrea.ms/schema/1.0/request-friend',
 | 
			
		||||
    authorize:      'http://activitystrea.ms/schema/1.0/authorize',
 | 
			
		||||
    reject:         'http://activitystrea.ms/schema/1.0/reject',
 | 
			
		||||
    unfollow:       'http://ostatus.org/schema/1.0/unfollow',
 | 
			
		||||
    block:          'http://mastodon.social/schema/1.0/block',
 | 
			
		||||
    unblock:        'http://mastodon.social/schema/1.0/unblock',
 | 
			
		||||
  }.freeze
 | 
			
		||||
 | 
			
		||||
  TYPES = {
 | 
			
		||||
| 
						 | 
				
			
			@ -38,6 +41,7 @@ class TagManager
 | 
			
		|||
  POCO_XMLNS  = 'http://portablecontacts.net/spec/1.0'
 | 
			
		||||
  DFRN_XMLNS  = 'http://purl.org/macgirvin/dfrn/1.0'
 | 
			
		||||
  OS_XMLNS    = 'http://ostatus.org/schema/1.0'
 | 
			
		||||
  MTDN_XMLNS  = 'http://mastodon.social/schema/1.0'
 | 
			
		||||
 | 
			
		||||
  def unique_tag(date, id, type)
 | 
			
		||||
    "tag:#{Rails.configuration.x.local_domain},#{date.strftime('%Y-%m-%d')}:objectId=#{id}:objectType=#{type}"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,6 +95,10 @@ class Account < ApplicationRecord
 | 
			
		|||
    follow_requests.where(target_account: other_account).exists?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def followers_domains
 | 
			
		||||
    followers.reorder('').select('DISTINCT accounts.domain').map(&:domain)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def local?
 | 
			
		||||
    domain.nil?
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,11 +12,11 @@ class Favourite < ApplicationRecord
 | 
			
		|||
  validates :status_id, uniqueness: { scope: :account_id }
 | 
			
		||||
 | 
			
		||||
  def verb
 | 
			
		||||
    :favorite
 | 
			
		||||
    destroyed? ? :unfavorite : :favorite
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def title
 | 
			
		||||
    "#{account.acct} favourited a status by #{status.account.acct}"
 | 
			
		||||
    destroyed? ? "#{account.acct} no longer favourites a status by #{status.account.acct}" : "#{account.acct} favourited a status by #{status.account.acct}"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  delegate :object_type, to: :target
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
 | 
			
		||||
class FollowRequest < ApplicationRecord
 | 
			
		||||
  include Paginable
 | 
			
		||||
  include Streamable
 | 
			
		||||
 | 
			
		||||
  belongs_to :account
 | 
			
		||||
  belongs_to :target_account, class_name: 'Account'
 | 
			
		||||
| 
						 | 
				
			
			@ -12,12 +13,47 @@ class FollowRequest < ApplicationRecord
 | 
			
		|||
  validates :account_id, uniqueness: { scope: :target_account_id }
 | 
			
		||||
 | 
			
		||||
  def authorize!
 | 
			
		||||
    @verb = :authorize
 | 
			
		||||
 | 
			
		||||
    account.follow!(target_account)
 | 
			
		||||
    MergeWorker.perform_async(target_account.id, account.id)
 | 
			
		||||
 | 
			
		||||
    destroy!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def reject!
 | 
			
		||||
    @verb = :reject
 | 
			
		||||
    destroy!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def verb
 | 
			
		||||
    destroyed? ? (@verb || :delete) : :request_friend
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def target
 | 
			
		||||
    target_account
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def object_type
 | 
			
		||||
    :person
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def hidden?
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def title
 | 
			
		||||
    if destroyed?
 | 
			
		||||
      case @verb
 | 
			
		||||
      when :authorize
 | 
			
		||||
        "#{target_account.acct} authorized #{account.acct}'s request to follow"
 | 
			
		||||
      when :reject
 | 
			
		||||
        "#{target_account.acct} rejected #{account.acct}'s request to follow"
 | 
			
		||||
      else
 | 
			
		||||
        "#{account.acct} withdrew the request to follow #{target_account.acct}"
 | 
			
		||||
      end
 | 
			
		||||
    else
 | 
			
		||||
      "#{account.acct} requested to follow #{target_account.acct}"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -76,7 +76,11 @@ class Status < ApplicationRecord
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def permitted?(other_account = nil)
 | 
			
		||||
    private_visibility? ? (account.id == other_account&.id || other_account&.following?(account)) : other_account.nil? || !account.blocking?(other_account)
 | 
			
		||||
    if private_visibility?
 | 
			
		||||
      (account.id == other_account&.id || other_account&.following?(account) || mentions.include?(other_account))
 | 
			
		||||
    else
 | 
			
		||||
      other_account.nil? || !account.blocking?(other_account)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def ancestors(account = nil)
 | 
			
		||||
| 
						 | 
				
			
			@ -153,6 +157,10 @@ class Status < ApplicationRecord
 | 
			
		|||
        where('1 = 1')
 | 
			
		||||
      elsif !account.nil? && target_account.blocking?(account)
 | 
			
		||||
        where('1 = 0')
 | 
			
		||||
      elsif !account.nil?
 | 
			
		||||
        joins('LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id')
 | 
			
		||||
          .where('mentions.account_id = ?', account.id)
 | 
			
		||||
          .where('statuses.visibility != ? OR mentions.id IS NOT NULL', Status.visibilities[:private])
 | 
			
		||||
      else
 | 
			
		||||
        where.not(visibility: :private)
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,7 @@ class StreamEntry < ApplicationRecord
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def targeted?
 | 
			
		||||
    [:follow, :unfollow, :block, :unblock, :share, :favorite].include? verb
 | 
			
		||||
    [:follow, :request_friend, :authorize, :unfollow, :block, :unblock, :share, :favorite].include? verb
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def target
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										11
									
								
								app/services/authorize_follow_service.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/services/authorize_follow_service.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class AuthorizeFollowService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
 | 
			
		||||
  def call(source_account, target_account)
 | 
			
		||||
    follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
 | 
			
		||||
    follow_request.authorize!
 | 
			
		||||
    NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), target_account.id, source_account.id) unless source_account.local?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class BlockService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
 | 
			
		||||
  def call(account, target_account)
 | 
			
		||||
    return if account.id == target_account.id
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +12,6 @@ class BlockService < BaseService
 | 
			
		|||
    block = account.block!(target_account)
 | 
			
		||||
 | 
			
		||||
    BlockWorker.perform_async(account.id, target_account.id)
 | 
			
		||||
    NotificationWorker.perform_async(block.stream_entry.id, target_account.id) unless target_account.local?
 | 
			
		||||
    NotificationWorker.perform_async(stream_entry_to_xml(block.stream_entry), account.id, target_account.id) unless target_account.local?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										8
									
								
								app/services/concerns/stream_entry_renderer.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/services/concerns/stream_entry_renderer.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module StreamEntryRenderer
 | 
			
		||||
  def stream_entry_to_xml(stream_entry)
 | 
			
		||||
    renderer = StreamEntriesController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https)
 | 
			
		||||
    renderer.render(:show, assigns: { stream_entry: stream_entry }, formats: [:atom])
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class FavouriteService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
 | 
			
		||||
  # Favourite a status and notify remote user
 | 
			
		||||
  # @param [Account] account
 | 
			
		||||
  # @param [Status] status
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +17,7 @@ class FavouriteService < BaseService
 | 
			
		|||
    if status.local?
 | 
			
		||||
      NotifyService.new.call(favourite.status.account, favourite)
 | 
			
		||||
    else
 | 
			
		||||
      NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id)
 | 
			
		||||
      NotificationWorker.perform_async(stream_entry_to_xml(favourite.stream_entry), account.id, status.account_id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    favourite
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,9 @@ class FetchRemoteAccountService < BaseService
 | 
			
		|||
 | 
			
		||||
    Rails.logger.debug "Going to webfinger #{username}@#{domain}"
 | 
			
		||||
 | 
			
		||||
    return FollowRemoteAccountService.new.call("#{username}@#{domain}")
 | 
			
		||||
    account = FollowRemoteAccountService.new.call("#{username}@#{domain}")
 | 
			
		||||
    UpdateRemoteProfileService.new.call(xml, account) unless account.nil?
 | 
			
		||||
    account
 | 
			
		||||
  rescue TypeError
 | 
			
		||||
    Rails.logger.debug "Unparseable URL given: #{url}"
 | 
			
		||||
    nil
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class FollowService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
 | 
			
		||||
  # Follow a remote user, notify remote user about the follow
 | 
			
		||||
  # @param [Account] source_account From which to follow
 | 
			
		||||
  # @param [String] uri User URI to follow in the form of username@domain
 | 
			
		||||
| 
						 | 
				
			
			@ -20,10 +22,14 @@ class FollowService < BaseService
 | 
			
		|||
  private
 | 
			
		||||
 | 
			
		||||
  def request_follow(source_account, target_account)
 | 
			
		||||
    return unless target_account.local?
 | 
			
		||||
 | 
			
		||||
    follow_request = FollowRequest.create!(account: source_account, target_account: target_account)
 | 
			
		||||
    NotifyService.new.call(target_account, follow_request)
 | 
			
		||||
 | 
			
		||||
    if target_account.local?
 | 
			
		||||
      NotifyService.new.call(target_account, follow_request)
 | 
			
		||||
    else
 | 
			
		||||
      NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), source_account.id, target_account.id)
 | 
			
		||||
      AfterRemoteFollowRequestWorker.perform_async(follow_request.id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    follow_request
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -34,8 +40,9 @@ class FollowService < BaseService
 | 
			
		|||
    if target_account.local?
 | 
			
		||||
      NotifyService.new.call(target_account, follow)
 | 
			
		||||
    else
 | 
			
		||||
      subscribe_service.call(target_account)
 | 
			
		||||
      NotificationWorker.perform_async(follow.stream_entry.id, target_account.id)
 | 
			
		||||
      subscribe_service.call(target_account) unless target_account.subscribed?
 | 
			
		||||
      NotificationWorker.perform_async(stream_entry_to_xml(follow.stream_entry), source_account.id, target_account.id)
 | 
			
		||||
      AfterRemoteFollowWorker.perform_async(follow.id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    MergeWorker.perform_async(target_account.id, source_account.id)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -106,7 +106,8 @@ class ProcessFeedService < BaseService
 | 
			
		|||
        text: content(entry),
 | 
			
		||||
        spoiler_text: content_warning(entry),
 | 
			
		||||
        created_at: published(entry),
 | 
			
		||||
        reply: thread?(entry)
 | 
			
		||||
        reply: thread?(entry),
 | 
			
		||||
        visibility: visibility_scope(entry)
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      if thread?(entry)
 | 
			
		||||
| 
						 | 
				
			
			@ -144,15 +145,9 @@ class ProcessFeedService < BaseService
 | 
			
		|||
 | 
			
		||||
    def mentions_from_xml(parent, xml)
 | 
			
		||||
      processed_account_ids = []
 | 
			
		||||
      public_visibility     = false
 | 
			
		||||
 | 
			
		||||
      xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link|
 | 
			
		||||
        if link['ostatus:object-type'] == TagManager::TYPES[:collection] && link['href'] == TagManager::COLLECTIONS[:public]
 | 
			
		||||
          public_visibility = true
 | 
			
		||||
          next
 | 
			
		||||
        elsif link['ostatus:object-type'] == TagManager::TYPES[:group]
 | 
			
		||||
          next
 | 
			
		||||
        end
 | 
			
		||||
        next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type']
 | 
			
		||||
 | 
			
		||||
        url = Addressable::URI.parse(link['href'])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -172,9 +167,6 @@ class ProcessFeedService < BaseService
 | 
			
		|||
        # So we can skip duplicate mentions
 | 
			
		||||
        processed_account_ids << mentioned_account.id
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      parent.visibility = public_visibility ? :public : :unlisted
 | 
			
		||||
      parent.save!
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def hashtags_from_xml(parent, xml)
 | 
			
		||||
| 
						 | 
				
			
			@ -230,6 +222,10 @@ class ProcessFeedService < BaseService
 | 
			
		|||
      xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || ''
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def visibility_scope(xml = @xml)
 | 
			
		||||
      xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content&.to_sym || :public
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def published(xml = @xml)
 | 
			
		||||
      xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,6 +29,12 @@ class ProcessInteractionService < BaseService
 | 
			
		|||
      case verb(xml)
 | 
			
		||||
      when :follow
 | 
			
		||||
        follow!(account, target_account) unless target_account.locked? || target_account.blocking?(account)
 | 
			
		||||
      when :request_friend
 | 
			
		||||
        follow_request!(account, target_account) unless !target_account.locked? || target_account.blocking?(account)
 | 
			
		||||
      when :authorize
 | 
			
		||||
        authorize_follow_request!(account, target_account)
 | 
			
		||||
      when :reject
 | 
			
		||||
        reject_follow_request!(account, target_account)
 | 
			
		||||
      when :unfollow
 | 
			
		||||
        unfollow!(account, target_account)
 | 
			
		||||
      when :favorite
 | 
			
		||||
| 
						 | 
				
			
			@ -72,6 +78,22 @@ class ProcessInteractionService < BaseService
 | 
			
		|||
    NotifyService.new.call(target_account, follow)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def follow_request!(account, target_account)
 | 
			
		||||
    follow_request = FollowRequest.create!(account: account, target_account: target_account)
 | 
			
		||||
    NotifyService.new.call(target_account, follow_request)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def authorize_follow_request!(account, target_account)
 | 
			
		||||
    follow_request = FollowRequest.find_by(account: target_account, target_account: account)
 | 
			
		||||
    follow_request&.authorize!
 | 
			
		||||
    SubscribeService.new.call(account) unless account.subscribed?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def reject_follow_request!(account, target_account)
 | 
			
		||||
    follow_request = FollowRequest.find_by(account: target_account, target_account: account)
 | 
			
		||||
    follow_request&.reject!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def unfollow!(account, target_account)
 | 
			
		||||
    account.unfollow!(target_account)
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class ProcessMentionsService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
 | 
			
		||||
  # Scan status for mentions and fetch remote mentioned users, create
 | 
			
		||||
  # local mention pointers, send Salmon notifications to mentioned
 | 
			
		||||
  # remote users
 | 
			
		||||
| 
						 | 
				
			
			@ -28,12 +30,10 @@ class ProcessMentionsService < BaseService
 | 
			
		|||
    status.mentions.each do |mention|
 | 
			
		||||
      mentioned_account = mention.account
 | 
			
		||||
 | 
			
		||||
      next if status.private_visibility? && (!mentioned_account.following?(status.account) || !mentioned_account.local?)
 | 
			
		||||
 | 
			
		||||
      if mentioned_account.local?
 | 
			
		||||
        NotifyService.new.call(mentioned_account, mention)
 | 
			
		||||
      else
 | 
			
		||||
        NotificationWorker.perform_async(status.stream_entry.id, mentioned_account.id)
 | 
			
		||||
        NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class ReblogService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
 | 
			
		||||
  # Reblog a status and notify its remote author
 | 
			
		||||
  # @param [Account] account Account to reblog from
 | 
			
		||||
  # @param [Status] reblogged_status Status to be reblogged
 | 
			
		||||
| 
						 | 
				
			
			@ -18,15 +20,9 @@ class ReblogService < BaseService
 | 
			
		|||
    if reblogged_status.local?
 | 
			
		||||
      NotifyService.new.call(reblog.reblog.account, reblog)
 | 
			
		||||
    else
 | 
			
		||||
      NotificationWorker.perform_async(reblog.stream_entry.id, reblog.reblog.account_id)
 | 
			
		||||
      NotificationWorker.perform_async(stream_entry_to_xml(reblog.stream_entry), account.id, reblog.reblog.account_id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    reblog
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def send_interaction_service
 | 
			
		||||
    @send_interaction_service ||= SendInteractionService.new
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										11
									
								
								app/services/reject_follow_service.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/services/reject_follow_service.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class RejectFollowService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
 | 
			
		||||
  def call(source_account, target_account)
 | 
			
		||||
    follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
 | 
			
		||||
    follow_request.reject!
 | 
			
		||||
    NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), target_account.id, source_account.id) unless source_account.local?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class RemoveStatusService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
 | 
			
		||||
  def call(status)
 | 
			
		||||
    remove_from_self(status) if status.account.local?
 | 
			
		||||
    remove_from_followers(status)
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +45,7 @@ class RemoveStatusService < BaseService
 | 
			
		|||
 | 
			
		||||
  def send_delete_salmon(account, status)
 | 
			
		||||
    return unless status.local?
 | 
			
		||||
    NotificationWorker.perform_async(status.stream_entry.id, account.id)
 | 
			
		||||
    NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, account.id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def remove_reblogs(status)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,27 +2,16 @@
 | 
			
		|||
 | 
			
		||||
class SendInteractionService < BaseService
 | 
			
		||||
  # Send an Atom representation of an interaction to a remote Salmon endpoint
 | 
			
		||||
  # @param [StreamEntry] stream_entry
 | 
			
		||||
  # @param [String] Entry XML
 | 
			
		||||
  # @param [Account] source_account
 | 
			
		||||
  # @param [Account] target_account
 | 
			
		||||
  def call(stream_entry, target_account)
 | 
			
		||||
    envelope = salmon.pack(entry_xml(stream_entry), stream_entry.account.keypair)
 | 
			
		||||
  def call(xml, source_account, target_account)
 | 
			
		||||
    envelope = salmon.pack(xml, source_account.keypair)
 | 
			
		||||
    salmon.post(target_account.salmon_url, envelope)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def entry_xml(stream_entry)
 | 
			
		||||
    Nokogiri::XML::Builder.new do |xml|
 | 
			
		||||
      entry(xml, true) do
 | 
			
		||||
        author(xml) do
 | 
			
		||||
          include_author xml, stream_entry.account
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        include_entry xml, stream_entry
 | 
			
		||||
      end
 | 
			
		||||
    end.to_xml
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def salmon
 | 
			
		||||
    @salmon ||= OStatus2::Salmon.new
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,12 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class UnblockService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
 | 
			
		||||
  def call(account, target_account)
 | 
			
		||||
    return unless account.blocking?(target_account)
 | 
			
		||||
 | 
			
		||||
    unblock = account.unblock!(target_account)
 | 
			
		||||
    NotificationWorker.perform_async(unblock.stream_entry.id, target_account.id) unless target_account.local?
 | 
			
		||||
    NotificationWorker.perform_async(stream_entry_to_xml(unblock.stream_entry), account.id, target_account.id) unless target_account.local?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,14 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class UnfavouriteService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
 | 
			
		||||
  def call(account, status)
 | 
			
		||||
    favourite = Favourite.find_by!(account: account, status: status)
 | 
			
		||||
    favourite.destroy!
 | 
			
		||||
 | 
			
		||||
    unless status.local?
 | 
			
		||||
      NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id)
 | 
			
		||||
      NotificationWorker.perform_async(stream_entry_to_xml(favourite.stream_entry), account.id, status.account_id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    favourite
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,14 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class UnfollowService < BaseService
 | 
			
		||||
  include StreamEntryRenderer
 | 
			
		||||
 | 
			
		||||
  # Unfollow and notify the remote user
 | 
			
		||||
  # @param [Account] source_account Where to unfollow from
 | 
			
		||||
  # @param [Account] target_account Which to unfollow
 | 
			
		||||
  def call(source_account, target_account)
 | 
			
		||||
    follow = source_account.unfollow!(target_account)
 | 
			
		||||
    NotificationWorker.perform_async(follow.stream_entry.id, target_account.id) unless target_account.local?
 | 
			
		||||
    NotificationWorker.perform_async(stream_entry_to_xml(follow.stream_entry), source_account.id, target_account.id) unless target_account.local?
 | 
			
		||||
    UnmergeWorker.perform_async(target_account.id, source_account.id)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ class UpdateRemoteProfileService < BaseService
 | 
			
		|||
    unless author_xml.nil?
 | 
			
		||||
      account.display_name = author_xml.at_xpath('./poco:displayName', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:displayName', poco: TagManager::POCO_XMLNS).nil?
 | 
			
		||||
      account.note         = author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).content unless author_xml.at_xpath('./poco:note', poco: TagManager::POCO_XMLNS).nil?
 | 
			
		||||
      account.locked       = author_xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content == 'private'
 | 
			
		||||
 | 
			
		||||
      unless account.suspended? || DomainBlock.find_by(domain: account.domain)&.reject_media?
 | 
			
		||||
        account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'] unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS).nil? || author_xml.at_xpath('./xmlns:link[@rel="avatar"]', xmlns: TagManager::XMLNS)['href'].blank?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,6 +32,7 @@
 | 
			
		|||
      = link_to t('about.learn_more'), about_more_path
 | 
			
		||||
      = link_to t('about.terms'), terms_path
 | 
			
		||||
      = link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
 | 
			
		||||
      = link_to t('about.other_instances'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md'
 | 
			
		||||
 | 
			
		||||
    = link_to t('about.get_started'), new_user_registration_path, class: 'button webapp-btn'
 | 
			
		||||
    = link_to t('auth.login'), new_user_session_path, class: 'button webapp-btn'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,10 @@
 | 
			
		|||
      %td= best_in_place @settings['site_contact_username'], :value, url: admin_setting_path(@settings['site_contact_username']), place_holder: 'Enter a username'
 | 
			
		||||
    %tr
 | 
			
		||||
      %td= best_in_place @settings['site_contact_email'], :value, url: admin_setting_path(@settings['site_contact_email']), place_holder: 'Enter a public e-mail address'
 | 
			
		||||
    %tr
 | 
			
		||||
      %td
 | 
			
		||||
        %strong Site title
 | 
			
		||||
      %td= best_in_place @settings['site_title'], :value, url: admin_setting_path(@settings['site_title'])
 | 
			
		||||
    %tr
 | 
			
		||||
      %td
 | 
			
		||||
        %strong Site description
 | 
			
		||||
| 
						 | 
				
			
			@ -33,4 +37,4 @@
 | 
			
		|||
        Displayed on extended information page
 | 
			
		||||
        %br/
 | 
			
		||||
        You can use HTML tags
 | 
			
		||||
      %td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description'])
 | 
			
		||||
      %td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description'])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@
 | 
			
		|||
 | 
			
		||||
    %title
 | 
			
		||||
      = "#{yield(:page_title)} - " if content_for?(:page_title)
 | 
			
		||||
      Mastodon
 | 
			
		||||
      = Setting.site_title
 | 
			
		||||
 | 
			
		||||
    = stylesheet_link_tag 'application', media: 'all'
 | 
			
		||||
    = csrf_meta_tags
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,18 @@
 | 
			
		|||
- content_for :page_title do
 | 
			
		||||
  = "##{@tag.name}"
 | 
			
		||||
 | 
			
		||||
.compact-header
 | 
			
		||||
  %h1<
 | 
			
		||||
    = link_to 'Mastodon', root_path
 | 
			
		||||
    %small= "##{@tag.name}"
 | 
			
		||||
 | 
			
		||||
- if @statuses.empty?
 | 
			
		||||
  .accounts-grid
 | 
			
		||||
    = render partial: 'accounts/nothing_here'
 | 
			
		||||
- else
 | 
			
		||||
  .activity-stream.h-feed
 | 
			
		||||
    = render partial: 'stream_entries/status', collection: @statuses, as: :status, cached: true
 | 
			
		||||
    = render partial: 'stream_entries/status', collection: @statuses, as: :status
 | 
			
		||||
 | 
			
		||||
.pagination
 | 
			
		||||
  - if @statuses.size == 20
 | 
			
		||||
- if @statuses.size == 20
 | 
			
		||||
  .pagination
 | 
			
		||||
    = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next_page', rel: 'next'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										17
									
								
								app/workers/after_remote_follow_request_worker.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/workers/after_remote_follow_request_worker.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class AfterRemoteFollowRequestWorker
 | 
			
		||||
  include Sidekiq::Worker
 | 
			
		||||
 | 
			
		||||
  sidekiq_options retry: 5
 | 
			
		||||
 | 
			
		||||
  def perform(follow_request_id)
 | 
			
		||||
    follow_request  = FollowRequest.find(follow_request_id)
 | 
			
		||||
    updated_account = FetchRemoteAccountService.new.call(follow_request.target_account.remote_url)
 | 
			
		||||
 | 
			
		||||
    return if updated_account.locked?
 | 
			
		||||
 | 
			
		||||
    follow_request.destroy
 | 
			
		||||
    FollowService.new.call(follow_request.account, updated_account.acct)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										17
									
								
								app/workers/after_remote_follow_worker.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/workers/after_remote_follow_worker.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class AfterRemoteFollowWorker
 | 
			
		||||
  include Sidekiq::Worker
 | 
			
		||||
 | 
			
		||||
  sidekiq_options retry: 5
 | 
			
		||||
 | 
			
		||||
  def perform(follow_id)
 | 
			
		||||
    follow          = Follow.find(follow_id)
 | 
			
		||||
    updated_account = FetchRemoteAccountService.new.call(follow.target_account.remote_url)
 | 
			
		||||
 | 
			
		||||
    return unless updated_account.locked?
 | 
			
		||||
 | 
			
		||||
    follow.destroy
 | 
			
		||||
    FollowService.new.call(follow.account, updated_account.acct)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ class NotificationWorker
 | 
			
		|||
 | 
			
		||||
  sidekiq_options retry: 5
 | 
			
		||||
 | 
			
		||||
  def perform(stream_entry_id, target_account_id)
 | 
			
		||||
    SendInteractionService.new.call(StreamEntry.find(stream_entry_id), Account.find(target_account_id))
 | 
			
		||||
  def perform(xml, source_account_id, target_account_id)
 | 
			
		||||
    SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id))
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,13 +8,18 @@ class Pubsubhubbub::DistributionWorker
 | 
			
		|||
  def perform(stream_entry_id)
 | 
			
		||||
    stream_entry = StreamEntry.find(stream_entry_id)
 | 
			
		||||
 | 
			
		||||
    return if stream_entry.hidden?
 | 
			
		||||
    # Most hidden stream entries should not be PuSHed,
 | 
			
		||||
    # but statuses need to be distributed to trusted
 | 
			
		||||
    # followers even when they are hidden
 | 
			
		||||
    return if stream_entry.hidden? && stream_entry.activity_type != 'Status'
 | 
			
		||||
 | 
			
		||||
    account  = stream_entry.account
 | 
			
		||||
    renderer = AccountsController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https)
 | 
			
		||||
    payload  = renderer.render(:show, assigns: { account: account, entries: [stream_entry] }, formats: [:atom])
 | 
			
		||||
    domains  = account.followers_domains
 | 
			
		||||
 | 
			
		||||
    Subscription.where(account: account).active.select('id').find_each do |subscription|
 | 
			
		||||
    Subscription.where(account: account).active.select('id, callback_url').find_each do |subscription|
 | 
			
		||||
      next unless domains.include?(Addressable::URI.parse(subscription.callback_url).host)
 | 
			
		||||
      Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload)
 | 
			
		||||
    end
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class PushNotificationWorker
 | 
			
		||||
  include Sidekiq::Worker
 | 
			
		||||
 | 
			
		||||
  def perform(notification_id)
 | 
			
		||||
    SendPushNotificationService.new.call(Notification.find(notification_id))
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ en:
 | 
			
		|||
    get_started: Get started
 | 
			
		||||
    learn_more: Learn more
 | 
			
		||||
    links: Links
 | 
			
		||||
    other_instances: Other instances
 | 
			
		||||
    source_code: Source code
 | 
			
		||||
    status_count_after: statuses
 | 
			
		||||
    status_count_before: Who authored
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
# config/app.yml for rails-settings-cached
 | 
			
		||||
defaults: &defaults
 | 
			
		||||
  site_title: 'Mastodon'
 | 
			
		||||
  site_description: ''
 | 
			
		||||
  site_extended_description: ''
 | 
			
		||||
  site_contact_username: ''
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,18 @@
 | 
			
		|||
List of Mastodon instances
 | 
			
		||||
List of Known Mastodon instances
 | 
			
		||||
==========================
 | 
			
		||||
 | 
			
		||||
* [mastodon.social](https://mastodon.social)
 | 
			
		||||
* [social.tchncs.de](https://social.tchncs.de)
 | 
			
		||||
* [on.vu](https://on.vu)
 | 
			
		||||
* [animalliberation.social](https://animalliberation.social)
 | 
			
		||||
* [socially.constructed.space](https://socially.constructed.space)
 | 
			
		||||
* [epiktistes.com](https://epiktistes.com)
 | 
			
		||||
* [toot.zone](https://toot.zone)
 | 
			
		||||
| Name | Theme/Notes, if applicable | Open Registrations |
 | 
			
		||||
| -------------|-------------|---|
 | 
			
		||||
| [mastodon.social](https://mastodon.social) |Flagship, quick updates|Yes|
 | 
			
		||||
| [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|
 | 
			
		||||
| [social.tchncs.de](https://social.tchncs.de)|N/A|Yes|
 | 
			
		||||
| [animalliberation.social](https://animalliberation.social) |Animal Rights|Yes|
 | 
			
		||||
| [socially.constructed.space](https://socially.constructed.space) |Single user|No|
 | 
			
		||||
| [epiktistes.com](https://epiktistes.com) |N/A|Yes|
 | 
			
		||||
| [toot.zone](https://toot.zone) |N/A|Yes|
 | 
			
		||||
| [on.vu](https://on.vu) | Appears defunct|No|
 | 
			
		||||
| [gay.crime.team](https://gay.crime.team) |N/A|Yes(?)|
 | 
			
		||||
| [gnusocial.me](https://gnusocial.me) |Yes, it's a mastodon instance now|Yes|
 | 
			
		||||
 | 
			
		||||
Let me know if you start running one so I can add it to the list!
 | 
			
		||||
 | 
			
		||||
Let me know if you start running one so I can add it to the list! (Alternatively, add it yourself as a pull request).
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ RSpec.describe Api::V1::FollowsController, type: :controller do
 | 
			
		|||
    before do
 | 
			
		||||
      stub_request(:get,  "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
 | 
			
		||||
      stub_request(:get,  "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt'))
 | 
			
		||||
      stub_request(:head, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(:status => 405, :body => "", :headers => {})
 | 
			
		||||
      stub_request(:get,  "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt'))
 | 
			
		||||
      stub_request(:get,  "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt'))
 | 
			
		||||
      stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ RSpec.describe AtomBuilderHelper, type: :helper do
 | 
			
		|||
 | 
			
		||||
  describe '#feed' do
 | 
			
		||||
    it 'creates a feed' do
 | 
			
		||||
      expect(used_in_builder { |xml| helper.feed(xml) }).to match '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0"/>'
 | 
			
		||||
      expect(used_in_builder { |xml| helper.feed(xml) }).to match '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0"/>'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue