logo

mastofe

My custom branche(s) on git.pleroma.social/pleroma/mastofe
commit: e6408b2e7ade3dac8dcf14bcda5b5c6a159fa74c
parent: 0afed995ce60dec11bc7718f83ca5afde86f6228
Author: Eugen Rochko <eugen@zeonfederated.com>
Date:   Sat, 11 Feb 2017 15:43:09 +0100

Merge branch 'feature-privacy-federation' into development

Diffstat:

Mapp/assets/javascripts/components/components/column_collapsable.jsx5+++--
Mapp/assets/javascripts/components/components/loading_indicator.jsx3+--
Mapp/assets/javascripts/components/components/media_gallery.jsx6++----
Mapp/assets/javascripts/components/components/video_player.jsx6++----
Mapp/assets/javascripts/components/features/account/components/header.jsx2+-
Mapp/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx7++-----
Mapp/assets/javascripts/components/features/home_timeline/components/column_settings.jsx8+++-----
Mapp/assets/javascripts/components/features/notifications/components/clear_column_button.jsx3+--
Mapp/assets/javascripts/components/features/notifications/components/column_settings.jsx12+++++-------
Mapp/assets/javascripts/components/features/notifications/components/notification.jsx20+++++---------------
Mapp/assets/javascripts/components/features/notifications/components/setting_toggle.jsx5++---
Mapp/assets/javascripts/components/features/status/components/card.jsx43++++++-------------------------------------
Mapp/assets/javascripts/components/features/status/components/detailed_status.jsx4++--
Mapp/assets/javascripts/components/features/ui/components/column_link.jsx1-
Mapp/assets/javascripts/components/features/ui/containers/modal_container.jsx9+++------
Mapp/assets/stylesheets/application.scss29+++++++++++++++++++++++++++++
Mapp/assets/stylesheets/components.scss135++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mapp/assets/stylesheets/stream_entries.scss8++++----
Mapp/controllers/api/v1/follow_requests_controller.rb4++--
Mapp/controllers/concerns/obfuscate_filename.rb1+
Mapp/helpers/atom_builder_helper.rb8++++++++
Mapp/lib/feed_manager.rb1-
Mapp/lib/tag_manager.rb22+++++++++++++---------
Mapp/models/account.rb4++++
Mapp/models/favourite.rb4++--
Mapp/models/follow_request.rb36++++++++++++++++++++++++++++++++++++
Mapp/models/status.rb10+++++++++-
Mapp/models/stream_entry.rb2+-
Aapp/services/authorize_follow_service.rb11+++++++++++
Mapp/services/block_service.rb4+++-
Aapp/services/concerns/stream_entry_renderer.rb8++++++++
Mapp/services/favourite_service.rb4+++-
Mapp/services/fetch_remote_account_service.rb4+++-
Mapp/services/follow_service.rb17++++++++++++-----
Mapp/services/process_feed_service.rb18+++++++-----------
Mapp/services/process_interaction_service.rb22++++++++++++++++++++++
Mapp/services/process_mentions_service.rb6+++---
Mapp/services/reblog_service.rb10+++-------
Aapp/services/reject_follow_service.rb11+++++++++++
Mapp/services/remove_status_service.rb4+++-
Mapp/services/send_interaction_service.rb19++++---------------
Mapp/services/unblock_service.rb4+++-
Mapp/services/unfavourite_service.rb4+++-
Mapp/services/unfollow_service.rb4+++-
Mapp/services/update_remote_profile_service.rb1+
Mapp/views/about/index.html.haml1+
Mapp/views/admin/settings/index.html.haml7+++++--
Mapp/views/layouts/application.html.haml2+-
Mapp/views/tags/show.html.haml14+++++++++++---
Aapp/workers/after_remote_follow_request_worker.rb17+++++++++++++++++
Aapp/workers/after_remote_follow_worker.rb17+++++++++++++++++
Mapp/workers/notification_worker.rb4++--
Mapp/workers/pubsubhubbub/distribution_worker.rb9+++++++--
Dapp/workers/push_notification_worker.rb11-----------
Mconfig/locales/en.yml1+
Mconfig/settings.yml1+
Mdocs/Using-Mastodon/List-of-Mastodon-instances.md24+++++++++++++++---------
Mspec/controllers/api/v1/follows_controller_spec.rb1+
Mspec/helpers/atom_builder_helper_spec.rb2+-
59 files changed, 451 insertions(+), 209 deletions(-)

diff --git a/app/assets/javascripts/components/components/column_collapsable.jsx b/app/assets/javascripts/components/components/column_collapsable.jsx @@ -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 }) => diff --git a/app/assets/javascripts/components/components/loading_indicator.jsx b/app/assets/javascripts/components/components/loading_indicator.jsx @@ -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> ); diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx @@ -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> diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx @@ -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> diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx @@ -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')) { diff --git a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx @@ -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> diff --git a/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx b/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx @@ -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)} /> diff --git a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx @@ -4,8 +4,7 @@ const iconStyle = { position: 'absolute', right: '48px', top: '0', - cursor: 'pointer', - background: '#2f3441' + cursor: 'pointer' }; const ClearColumnButton = ({ onClick }) => ( diff --git a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx @@ -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} /> diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx @@ -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 }} /> diff --git a/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx b/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx @@ -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> ); diff --git a/app/assets/javascripts/components/features/status/components/card.jsx b/app/assets/javascripts/components/features/status/components/card.jsx @@ -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> ); diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -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> diff --git a/app/assets/javascripts/components/features/ui/components/column_link.jsx b/app/assets/javascripts/components/features/ui/components/column_link.jsx @@ -4,7 +4,6 @@ const outerStyle = { display: 'block', padding: '15px', fontSize: '16px', - color: '#fff', textDecoration: 'none' }; diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx @@ -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 ( diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss @@ -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'; diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss @@ -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; +} + diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss @@ -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; } } diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb @@ -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 diff --git a/app/controllers/concerns/obfuscate_filename.rb b/app/controllers/concerns/obfuscate_filename.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module ObfuscateFilename extend ActiveSupport::Concern diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb @@ -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 diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb @@ -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 diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb @@ -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}" diff --git a/app/models/account.rb b/app/models/account.rb @@ -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 diff --git a/app/models/favourite.rb b/app/models/favourite.rb @@ -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 diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb @@ -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 diff --git a/app/models/status.rb b/app/models/status.rb @@ -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 diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb @@ -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 diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb @@ -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 diff --git a/app/services/block_service.rb b/app/services/block_service.rb @@ -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 diff --git a/app/services/concerns/stream_entry_renderer.rb b/app/services/concerns/stream_entry_renderer.rb @@ -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 diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb @@ -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 diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb @@ -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 diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb @@ -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) diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb @@ -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 diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb @@ -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 diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb @@ -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 diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb @@ -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 diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb @@ -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 diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb @@ -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) diff --git a/app/services/send_interaction_service.rb b/app/services/send_interaction_service.rb @@ -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 diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb @@ -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 diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb @@ -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 diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb @@ -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 diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb @@ -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? diff --git a/app/views/about/index.html.haml b/app/views/about/index.html.haml @@ -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' diff --git a/app/views/admin/settings/index.html.haml b/app/views/admin/settings/index.html.haml @@ -17,6 +17,10 @@ %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 %br/ Displayed as a paragraph on the frontpage and used as a meta tag. @@ -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'])- \ No newline at end of file + %td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description']) diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml @@ -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 diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml @@ -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' diff --git a/app/workers/after_remote_follow_request_worker.rb b/app/workers/after_remote_follow_request_worker.rb @@ -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 diff --git a/app/workers/after_remote_follow_worker.rb b/app/workers/after_remote_follow_worker.rb @@ -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 diff --git a/app/workers/notification_worker.rb b/app/workers/notification_worker.rb @@ -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 diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb @@ -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 diff --git a/app/workers/push_notification_worker.rb b/app/workers/push_notification_worker.rb @@ -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 diff --git a/config/locales/en.yml b/config/locales/en.yml @@ -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 diff --git a/config/settings.yml b/config/settings.yml @@ -1,5 +1,6 @@ # config/app.yml for rails-settings-cached defaults: &defaults + site_title: 'Mastodon' site_description: '' site_extended_description: '' site_contact_username: '' diff --git a/docs/Using-Mastodon/List-of-Mastodon-instances.md b/docs/Using-Mastodon/List-of-Mastodon-instances.md @@ -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). diff --git a/spec/controllers/api/v1/follows_controller_spec.rb b/spec/controllers/api/v1/follows_controller_spec.rb @@ -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 => {}) diff --git a/spec/helpers/atom_builder_helper_spec.rb b/spec/helpers/atom_builder_helper_spec.rb @@ -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