Merge branch 'feature-privacy-federation' into development
This commit is contained in:
		| @ -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 | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user