diff options
Diffstat (limited to 'app')
74 files changed, 698 insertions, 525 deletions
diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx index 980b7d63e..11e814e1f 100644 --- a/app/assets/javascripts/components/actions/notifications.jsx +++ b/app/assets/javascripts/components/actions/notifications.jsx @@ -61,6 +61,8 @@ export function refreshNotifications() { params.since_id = ids.first().get('id'); } + params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); + api(getState).get('/api/v1/notifications', { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); @@ -105,11 +107,11 @@ export function expandNotifications() { dispatch(expandNotificationsRequest()); - api(getState).get(url, { - params: { - limit: 5 - } - }).then(response => { + const params = {}; + + params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); + + api(getState).get(url, params).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx index 7a1c9f5ce..782cf382d 100644 --- a/app/assets/javascripts/components/components/account.jsx +++ b/app/assets/javascripts/components/components/account.jsx @@ -65,7 +65,7 @@ const Account = React.createClass({ <div className='account'> <div style={{ display: 'flex' }}> <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> - <div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={36} /></div> + <div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={36} /></div> <DisplayName account={account} /> </Permalink> diff --git a/app/assets/javascripts/components/components/avatar.jsx b/app/assets/javascripts/components/components/avatar.jsx index 0237a1904..673b1a247 100644 --- a/app/assets/javascripts/components/components/avatar.jsx +++ b/app/assets/javascripts/components/components/avatar.jsx @@ -1,103 +1,18 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; -// From: http://stackoverflow.com/a/18320662 -const resample = (canvas, width, height, resize_canvas) => { - let width_source = canvas.width; - let height_source = canvas.height; - width = Math.round(width); - height = Math.round(height); - - let ratio_w = width_source / width; - let ratio_h = height_source / height; - let ratio_w_half = Math.ceil(ratio_w / 2); - let ratio_h_half = Math.ceil(ratio_h / 2); - - let ctx = canvas.getContext("2d"); - let img = ctx.getImageData(0, 0, width_source, height_source); - let img2 = ctx.createImageData(width, height); - let data = img.data; - let data2 = img2.data; - - for (let j = 0; j < height; j++) { - for (let i = 0; i < width; i++) { - let x2 = (i + j * width) * 4; - let weight = 0; - let weights = 0; - let weights_alpha = 0; - let gx_r = 0; - let gx_g = 0; - let gx_b = 0; - let gx_a = 0; - let center_y = (j + 0.5) * ratio_h; - let yy_start = Math.floor(j * ratio_h); - let yy_stop = Math.ceil((j + 1) * ratio_h); - - for (let yy = yy_start; yy < yy_stop; yy++) { - let dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half; - let center_x = (i + 0.5) * ratio_w; - let w0 = dy * dy; //pre-calc part of w - let xx_start = Math.floor(i * ratio_w); - let xx_stop = Math.ceil((i + 1) * ratio_w); - - for (let xx = xx_start; xx < xx_stop; xx++) { - let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half; - let w = Math.sqrt(w0 + dx * dx); - - if (w >= 1) { - // pixel too far - continue; - } - - // hermite filter - weight = 2 * w * w * w - 3 * w * w + 1; - let pos_x = 4 * (xx + yy * width_source); - - // alpha - gx_a += weight * data[pos_x + 3]; - weights_alpha += weight; - - // colors - if (data[pos_x + 3] < 255) - weight = weight * data[pos_x + 3] / 250; - - gx_r += weight * data[pos_x]; - gx_g += weight * data[pos_x + 1]; - gx_b += weight * data[pos_x + 2]; - weights += weight; - } - } - - data2[x2] = gx_r / weights; - data2[x2 + 1] = gx_g / weights; - data2[x2 + 2] = gx_b / weights; - data2[x2 + 3] = gx_a / weights_alpha; - } - } - - // clear and resize canvas - if (resize_canvas === true) { - canvas.width = width; - canvas.height = height; - } else { - ctx.clearRect(0, 0, width_source, height_source); - } - - // draw - ctx.putImageData(img2, 0, 0); -}; - const Avatar = React.createClass({ propTypes: { src: React.PropTypes.string.isRequired, + staticSrc: React.PropTypes.string, size: React.PropTypes.number.isRequired, style: React.PropTypes.object, - animated: React.PropTypes.bool + animate: React.PropTypes.bool }, getDefaultProps () { return { - animated: true + animate: false }; }, @@ -117,38 +32,30 @@ const Avatar = React.createClass({ this.setState({ hovering: false }); }, - handleLoad () { - this.canvas.width = this.image.naturalWidth; - this.canvas.height = this.image.naturalHeight; - this.canvas.getContext('2d').drawImage(this.image, 0, 0); - - resample(this.canvas, this.props.size * window.devicePixelRatio, this.props.size * window.devicePixelRatio, true); - }, - - setImageRef (c) { - this.image = c; - }, - - setCanvasRef (c) { - this.canvas = c; - }, - render () { + const { src, size, staticSrc, animate } = this.props; const { hovering } = this.state; - if (this.props.animated) { - return ( - <div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}> - <img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ borderRadius: '4px' }} /> - </div> - ); + const style = { + ...this.props.style, + width: `${size}px`, + height: `${size}px`, + backgroundSize: `${size}px ${size}px` + }; + + if (hovering || animate) { + style.backgroundImage = `url(${src})`; + } else { + style.backgroundImage = `url(${staticSrc})`; } return ( - <div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}> - <img ref={this.setImageRef} onLoad={this.handleLoad} src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ position: 'absolute', top: '0', left: '0', opacity: hovering ? '1' : '0', borderRadius: '4px' }} /> - <canvas ref={this.setCanvasRef} style={{ borderRadius: '4px', width: this.props.size, height: this.props.size, opacity: hovering ? '0' : '1' }} /> - </div> + <div + className='avatar' + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + style={style} + /> ); } diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx index 60bf531e5..c4d5f829b 100644 --- a/app/assets/javascripts/components/components/status.jsx +++ b/app/assets/javascripts/components/components/status.jsx @@ -91,7 +91,7 @@ const Status = React.createClass({ <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}> <div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}> - <Avatar src={status.getIn(['account', 'avatar'])} size={48} /> + <Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /> </div> <DisplayName account={status.get('account')} /> diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx index 6c25afdea..9cf03bb32 100644 --- a/app/assets/javascripts/components/components/status_content.jsx +++ b/app/assets/javascripts/components/components/status_content.jsx @@ -36,6 +36,7 @@ const StatusContent = React.createClass({ if (mention) { link.addEventListener('click', this.onMentionClick.bind(this, mention), false); + link.setAttribute('title', mention.get('acct')); } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); } else if (media) { @@ -91,7 +92,7 @@ const StatusContent = React.createClass({ const { status } = this.props; const { hidden } = this.state; - const content = { __html: emojify(status.get('content')) }; + const content = { __html: emojify(status.get('content')).replace(/\n/g, '') }; const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) }; const directionStyle = { direction: 'ltr' }; @@ -125,7 +126,7 @@ const StatusContent = React.createClass({ <div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} /> </div> ); - } else { + } else if (this.props.onClick) { return ( <div className='status__content' @@ -135,6 +136,14 @@ const StatusContent = React.createClass({ dangerouslySetInnerHTML={content} /> ); + } else { + return ( + <div + className='status__content' + style={{ ...directionStyle }} + dangerouslySetInnerHTML={content} + /> + ); } }, diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 00f20074d..fea8b1594 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -48,6 +48,8 @@ import hu from 'react-intl/locale-data/hu'; import uk from 'react-intl/locale-data/uk'; import fi from 'react-intl/locale-data/fi'; import eo from 'react-intl/locale-data/eo'; +import ru from 'react-intl/locale-data/ru'; + import getMessagesForLocale from '../locales'; import { hydrateStore } from '../actions/store'; import createStream from '../stream'; @@ -60,7 +62,9 @@ const browserHistory = useRouterHistory(createBrowserHistory)({ basename: '/web' }); -addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo]); + +addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo, ...ru]); + const Mastodon = React.createClass({ diff --git a/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx index 5591b45cf..9e05193fb 100644 --- a/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx +++ b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx @@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; const AutosuggestAccount = ({ account }) => ( <div style={{ overflow: 'hidden' }} className='autosuggest-account'> - <div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div> + <div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={18} /></div> <DisplayName account={account} /> </div> ); diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx index b016d3f28..cb4b62f6c 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -83,11 +83,23 @@ const ComposeForm = React.createClass({ this.props.onChangeSpoilerText(e.target.value); }, + componentWillReceiveProps (nextProps) { + // If this is the update where we've finished uploading, + // save the last caret position so we can restore it below! + if (!nextProps.is_uploading && this.props.is_uploading) { + this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart; + } + }, + componentDidUpdate (prevProps) { - if (this.props.focusDate !== prevProps.focusDate) { - // If replying to zero or one users, places the cursor at the end of the textbox. - // If replying to more than one user, selects any usernames past the first; - // this provides a convenient shortcut to drop everyone else from the conversation. + // This statement does several things: + // - If we're beginning a reply, and, + // - Replying to zero or one users, places the cursor at the end of the textbox. + // - Replying to more than one user, selects any usernames past the first; + // this provides a convenient shortcut to drop everyone else from the conversation. + // - If we've just finished uploading an image, and have a saved caret position, + // restores the cursor to that position after the text changes! + if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) { let selectionEnd, selectionStart; if (this.props.preselectDate !== prevProps.preselectDate) { @@ -118,7 +130,7 @@ const ComposeForm = React.createClass({ render () { const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props; - const disabled = this.props.is_submitting || this.props.is_uploading; + const disabled = this.props.is_submitting; let publishText = ''; let privacyWarning = ''; diff --git a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx index 1920b29bf..fa577ce26 100644 --- a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx @@ -47,7 +47,7 @@ const EmojiPickerDropdown = React.createClass({ </DropdownTrigger> <DropdownContent className='dropdown__left'> - <EmojiPicker emojione={settings} onChange={this.handleChange} /> + <EmojiPicker emojione={settings} onChange={this.handleChange} search={true} /> </DropdownContent> </Dropdown> ); diff --git a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx index 076ac7cbb..1a748a23c 100644 --- a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx +++ b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx @@ -17,7 +17,7 @@ const NavigationBar = React.createClass({ render () { return ( <div className='navigation-bar'> - <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink> + <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink> <div style={{ flex: '1 1 auto', marginLeft: '8px' }}> <strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong> diff --git a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx index a72bd32c2..11a89449e 100644 --- a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx +++ b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx @@ -50,7 +50,7 @@ const ReplyIndicator = React.createClass({ <div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div> <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}> - <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} /></div> + <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div> <DisplayName account={status.get('account')} /> </a> </div> 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 index 1766655c2..9c713287c 100644 --- a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx +++ b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx @@ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => { <div> <div style={outerStyle}> <Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}> - <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={48} /></div> + <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div> <DisplayName account={account} /> </Permalink> 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 index 62c3e61e0..debbfd01f 100644 --- 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,16 +4,6 @@ const messages = defineMessages({ clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' } }); -const iconStyle = { - fontSize: '16px', - padding: '15px', - position: 'absolute', - right: '48px', - top: '0', - cursor: 'pointer', - zIndex: '2' -}; - const ClearColumnButton = React.createClass({ propTypes: { @@ -25,7 +15,7 @@ const ClearColumnButton = React.createClass({ const { intl } = this.props; return ( - <div title={intl.formatMessage(messages.clear)} className='column-icon' tabIndex='0' style={iconStyle} onClick={this.onClick}> + <div title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}> <i className='fa fa-eraser' /> </div> ); diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx index 0de4df52e..0607466d0 100644 --- a/app/assets/javascripts/components/features/notifications/components/notification.jsx +++ b/app/assets/javascripts/components/features/notifications/components/notification.jsx @@ -21,7 +21,7 @@ const Notification = React.createClass({ renderFollow (account, link) { return ( - <div className='notification'> + <div className='notification notification-follow'> <div className='notification__message'> <div style={{ position: 'absolute', 'left': '-26px'}}> <i className='fa fa-fw fa-user-plus' /> @@ -41,7 +41,7 @@ const Notification = React.createClass({ renderFavourite (notification, link) { return ( - <div className='notification'> + <div className='notification notification-favourite'> <div className='notification__message'> <div style={{ position: 'absolute', 'left': '-26px'}}> <i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} /> @@ -57,7 +57,7 @@ const Notification = React.createClass({ renderReblog (notification, link) { return ( - <div className='notification'> + <div className='notification notification-reblog'> <div className='notification__message'> <div style={{ position: 'absolute', 'left': '-26px'}}> <i className='fa fa-fw fa-retweet' /> @@ -76,7 +76,7 @@ const Notification = React.createClass({ const account = notification.get('account'); const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />; + const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />; switch(notification.get('type')) { case 'follow': diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx index caa46ff3c..2da57252e 100644 --- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx +++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -54,7 +54,7 @@ const DetailedStatus = React.createClass({ return ( <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> + <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div> <DisplayName account={status.get('account')} /> </a> diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx index fdd9c0e00..568422ff3 100644 --- a/app/assets/javascripts/components/locales/fr.jsx +++ b/app/assets/javascripts/components/locales/fr.jsx @@ -14,6 +14,7 @@ const fr = { "status.show_less": "Replier", "status.open": "Déplier ce status", "status.report": "Signaler @{name}", + "status.load_more": "Charger plus", "video_player.toggle_sound": "Mettre/Couper le son", "account.mention": "Mentionner", "account.edit_profile": "Modifier le profil", @@ -41,6 +42,7 @@ const fr = { "column.notifications": "Notifications", "column.blocks": "Utilisateurs bloqués", "column.favourites": "Favoris", + "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.", "tabs_bar.compose": "Composer", "tabs_bar.home": "Accueil", "tabs_bar.mentions": "Mentions", diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx index 1e7b8b548..f9e1fe5bd 100644 --- a/app/assets/javascripts/components/locales/index.jsx +++ b/app/assets/javascripts/components/locales/index.jsx @@ -7,6 +7,8 @@ import pt from './pt'; import uk from './uk'; import fi from './fi'; import eo from './eo'; +import ru from './ru'; + const locales = { en, @@ -17,7 +19,9 @@ const locales = { pt, uk, fi, - eo + eo, + ru + }; export default function getMessagesForLocale (locale) { diff --git a/app/assets/javascripts/components/locales/pt.jsx b/app/assets/javascripts/components/locales/pt.jsx index d68724b13..8d1b88c75 100644 --- a/app/assets/javascripts/components/locales/pt.jsx +++ b/app/assets/javascripts/components/locales/pt.jsx @@ -2,54 +2,71 @@ const pt = { "column_back_button.label": "Voltar", "lightbox.close": "Fechar", "loading_indicator.label": "Carregando...", - "status.mention": "Menção", - "status.delete": "Deletar", + "status.mention": "Mencionar @{name}", + "status.delete": "Eliminar", "status.reply": "Responder", - "status.reblog": "Reblogar", - "status.favourite": "Favoritar", - "status.reblogged_by": "{name} reblogou", - "video_player.toggle_sound": "Alterar som", - "account.mention": "Menção", + "status.reblog": "Partilhar", + "status.favourite": "Adicionar aos favoritos", + "status.reblogged_by": "{name} partilhou", + "status.sensitive_warning": "Conteúdo sensível", + "status.sensitive_toggle": "Clique para ver", + "status.show_more": "Mostrar mais", + "status.show_less": "Mostrar menos", + "status.open": "Expandir", + "status.report": "Reportar @{name}", + "video_player.toggle_sound": "Ligar/Desligar som", + "account.mention": "Mencionar @{name}", "account.edit_profile": "Editar perfil", - "account.unblock": "Desbloquear", - "account.unfollow": "Unfollow", - "account.block": "Bloquear", + "account.unblock": "Não bloquear @{name}", + "account.unfollow": "Não seguir", + "account.block": "Bloquear @{name}", "account.follow": "Seguir", - "account.block": "Bloquear", "account.posts": "Posts", "account.follows": "Segue", "account.followers": "Seguidores", - "account.follows_you": "Segue você", + "account.follows_you": "É teu seguidor", + "account.requested": "A aguardar aprovação", "getting_started.heading": "Primeiros passos", - "getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão entrando um endereço similar a e-mail no campo no topo da barra lateral.", + "getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão colocando um endereço similar a e-mail no campo no topo da barra lateral.", "getting_started.about_shortcuts": "Se o usuário alvo está no mesmo domínio, só o nome funcionará. A mesma regra se aplica a mencionar pessoas nas postagens.", - "getting_started.about_developer": "O desenvolvedor desse projeto pode ser seguido em Gargron@mastodon.social", + "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.", "column.home": "Home", - "column.mentions": "Menções", + "column.community": "Local", "column.public": "Público", - "tabs_bar.compose": "Compôr", + "column.notifications": "Notificações", + "tabs_bar.compose": "Criar", "tabs_bar.home": "Home", "tabs_bar.mentions": "Menções", "tabs_bar.public": "Público", "tabs_bar.notifications": "Notificações", - "compose_form.placeholder": "Que estás pensando?", + "compose_form.placeholder": "Em que estás a pensar?", "compose_form.publish": "Publicar", - "compose_form.sensitive": "Marcar conteúdo como sensível", - "compose_form.unlisted": "Modo não-listado", + "compose_form.sensitive": "Media com conteúdo sensível", + "compose_form.spoiler": "Esconder texto com aviso", + "compose_form.private": "Tornar privado", + "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.", + "compose_form.unlisted": "Não mostrar na listagem pública", "navigation_bar.edit_profile": "Editar perfil", "navigation_bar.preferences": "Preferências", - "navigation_bar.public_timeline": "Timeline Pública", - "navigation_bar.logout": "Logout", + "navigation_bar.community_timeline": "Local", + "navigation_bar.public_timeline": "Público", + "navigation_bar.logout": "Sair", "reply_indicator.cancel": "Cancelar", - "search.placeholder": "Busca", + "search.placeholder": "Pesquisar", "search.account": "Conta", "search.hashtag": "Hashtag", "upload_button.label": "Adicionar media", - "upload_form.undo": "Desfazer", - "notification.follow": "{name} seguiu você", - "notification.favourite": "{name} favoritou seu post", - "notification.reblog": "{name} reblogou o seu post", - "notification.mention": "{name} mecionou você" + "upload_form.undo": "Anular", + "notification.follow": "{name} seguiu-te", + "notification.favourite": "{name} adicionou o teu post aos favoritos", + "notification.reblog": "{name} partilhou o teu post", + "notification.mention": "{name} mencionou-te", + "notifications.column_settings.alert": "Notificações no computador", + "notifications.column_settings.show": "Mostrar nas colunas", + "notifications.column_settings.follow": "Novos seguidores:", + "notifications.column_settings.favourite": "Favoritos:", + "notifications.column_settings.mention": "Menções:", + "notifications.column_settings.reblog": "Partilhas:", }; export default pt; diff --git a/app/assets/javascripts/components/locales/ru.jsx b/app/assets/javascripts/components/locales/ru.jsx new file mode 100644 index 000000000..e109005a7 --- /dev/null +++ b/app/assets/javascripts/components/locales/ru.jsx @@ -0,0 +1,68 @@ +const ru = { + "column_back_button.label": "Назад", + "lightbox.close": "Закрыть", + "loading_indicator.label": "Загрузка...", + "status.mention": "Упомянуть @{name}", + "status.delete": "Удалить", + "status.reply": "Ответить", + "status.reblog": "Продвинуть", + "status.favourite": "Нравится", + "status.reblogged_by": "{name} продвинул(а)", + "status.sensitive_warning": "Чувствительный контент", + "status.sensitive_toggle": "Нажмите для просмотра", + "video_player.toggle_sound": "Вкл./выкл. звук", + "account.mention": "Упомянуть @{name}", + "account.edit_profile": "Изменить профиль", + "account.unblock": "Разблокировать @{name}", + "account.unfollow": "Отписаться", + "account.block": "Блокировать @{name}", + "account.follow": "Подписаться", + "account.posts": "Посты", + "account.follows": "Подписки", + "account.followers": "Подписчики", + "account.follows_you": "Подписан(а) на Вас", + "account.requested": "Ожидает подтверждения", + "getting_started.heading": "Добро пожаловать", + "getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.", + "getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.", + "getting_started.open_source_notice": "Mastodon - программа с открытым исходным кодом. Вы можете помочь проекту или сообщить о проблемах на GitHub по адресу {github}. {apps}.", + "column.home": "Главная", + "column.community": "Локальная лента", + "column.public": "Глобальная лента", + "column.notifications": "Уведомления", + "tabs_bar.compose": "Написать", + "tabs_bar.home": "Главная", + "tabs_bar.mentions": "Упоминания", + "tabs_bar.public": "Глобальная лента", + "tabs_bar.notifications": "Уведомления", + "compose_form.placeholder": "О чем Вы думаете?", + "compose_form.publish": "Протрубить", + "compose_form.sensitive": "Отметить как чувствительный контент", + "compose_form.spoiler": "Скрыть текст за предупреждением", + "compose_form.private": "Отметить как приватное", + "compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.", + "compose_form.unlisted": "Не отображать в публичных лентах", + "navigation_bar.edit_profile": "Изменить профиль", + "navigation_bar.preferences": "Опции", + "navigation_bar.community_timeline": "Локальная лента", + "navigation_bar.public_timeline": "Глобальная лента", + "navigation_bar.logout": "Выйти", + "reply_indicator.cancel": "Отмена", + "search.placeholder": "Поиск", + "search.account": "Аккаунт", + "search.hashtag": "Хэштег", + "upload_button.label": "Добавить медиаконтент", + "upload_form.undo": "Отменить", + "notification.follow": "{name} подписался(-лась) на Вас", + "notification.favourite": "{name} понравился Ваш статус", + "notification.reblog": "{name} продвинул(а) Ваш статус", + "notification.mention": "{name} упомянул(а) Вас", + "notifications.column_settings.alert": "Десктопные уведомления", + "notifications.column_settings.show": "Показывать в колонке", + "notifications.column_settings.follow": "Новые подписчики:", + "notifications.column_settings.favourite": "Нравится:", + "notifications.column_settings.mention": "Упоминания:", + "notifications.column_settings.reblog": "Продвижения:", +}; + +export default ru; diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss index b3ae33500..50181d86e 100644 --- a/app/assets/stylesheets/accounts.scss +++ b/app/assets/stylesheets/accounts.scss @@ -72,6 +72,7 @@ position: relative; z-index: 2; flex-direction: row; + background: rgba(0,0,0,0.5); } .details-counters { @@ -83,7 +84,7 @@ .counter { width: 80px; color: $color3; - padding: 0 10px; + padding: 5px 10px 0px; margin-bottom: 10px; border-right: 1px solid $color3; cursor: default; @@ -173,7 +174,7 @@ text-align: center; overflow: hidden; - a, .current, .next_page, .previous_page, .gap { + a, .current, .page, .gap { font-size: 14px; color: $color5; font-weight: 500; @@ -193,12 +194,12 @@ cursor: default; } - .previous_page, .next_page { + .prev, .next { text-transform: uppercase; color: $color2; } - .previous_page { + .prev { float: left; padding-left: 0; @@ -208,7 +209,7 @@ } } - .next_page { + .next { float: right; padding-right: 0; @@ -226,11 +227,11 @@ @media screen and (max-width: 360px) { padding: 30px 20px; - a, .current, .next_page, .previous_page, .gap { + a, .current, .next, .prev, .gap { display: none; } - .next_page, .previous_page { + .next, .prev { display: inline-block; } } diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index d31f148a2..316398874 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1,7 +1,7 @@ @import 'variables'; .app-body{ - -ms-overflow-style: -ms-autohiding-scrollbar; + -ms-overflow-style: -ms-autohiding-scrollbar; } .button { @@ -49,6 +49,22 @@ } } +.column-icon-clear { + font-size: 16px; + padding: 15px; + position: absolute; + right: 48px; + top: 0; + cursor: pointer; + z-index: 2; +} + +@media screen and (min-width: 1024px) { + .column-icon-clear { + top: 10px; + } +} + .icon-button { display: inline-block; padding: 0; @@ -149,6 +165,14 @@ } } +.avatar { + border-radius: 4px; + background: transparent no-repeat; + background-position: 50%; + background-clip: padding-box; + position: relative; +} + .lightbox .icon-button { color: $color1; } @@ -714,7 +738,7 @@ a.status__content__spoiler-link { @media screen and (min-width: 360px) { .columns-area { - margin: 10px; + padding: 10px; } } @@ -722,9 +746,12 @@ a.status__content__spoiler-link { width: 330px; position: relative; box-sizing: border-box; - background: $color1; display: flex; flex-direction: column; + + > .scrollable { + background: $color1; + } } .ui { @@ -756,6 +783,58 @@ a.status__content__spoiler-link { border-bottom: 2px solid transparent; } +.column, .drawer { + flex: 1 1 100%; + overflow: hidden; +} + +@media screen and (min-width: 360px) { + .tabs-bar { + margin: 10px; + margin-bottom: 0; + } + + .search { + margin-bottom: 10px; + } +} + +@media screen and (max-width: 1024px) { + .column, .drawer { + width: 100%; + padding: 0; + } + + .columns-area { + flex-direction: column; + } + + .search__input, .autosuggest-textarea__textarea { + font-size: 16px; + } +} + +@media screen and (min-width: 1024px) { + .columns-area { + padding: 0; + } + + .column, .drawer { + flex: 0 0 auto; + padding: 10px; + padding-left: 5px; + padding-right: 5px; + + &:first-child { + padding-left: 10px; + } + + &:last-child { + padding-right: 10px; + } + } +} + @media screen and (min-width: 2560px) { .columns-area { justify-content: center; @@ -815,37 +894,6 @@ a.status__content__spoiler-link { } } -.column, .drawer { - margin-left: 5px; - margin-right: 5px; - flex: 0 0 auto; - overflow: hidden; -} - -.column:first-child, .drawer:first-child { - margin-left: 0; -} - -.column:last-child, .drawer:last-child { - margin-right: 0; -} - -@media screen and (max-width: 1024px) { - .column, .drawer { - width: 100%; - margin: 0; - flex: 1 1 100%; - } - - .columns-area { - flex-direction: column; - } - - .search__input, .autosuggest-textarea__textarea { - font-size: 16px; - } -} - .tabs-bar { display: flex; background: lighten($color1, 8%); @@ -856,17 +904,18 @@ a.status__content__spoiler-link { .tabs-bar__link { display: block; flex: 1 1 auto; - padding: 10px 5px; + padding: 15px 10px; color: $color5; text-decoration: none; text-align: center; - font-size:12px; + font-size: 14px; font-weight: 500; border-bottom: 2px solid lighten($color1, 8%); transition: all 200ms linear; .fa { font-weight: 400; + font-size: 16px; } &.active { @@ -880,27 +929,13 @@ a.status__content__spoiler-link { } span { + margin-left: 5px; display: none; } } -@media screen and (min-width: 360px) { - .tabs-bar { - margin: 10px; - margin-bottom: 0; - } - - .search { - margin-bottom: 10px; - } -} - @media screen and (min-width: 600px) { .tabs-bar__link { - .fa { - margin-right: 5px; - } - span { display: inline; } @@ -1362,12 +1397,15 @@ button.icon-button.active i.fa-retweet { .empty-column-indicator { color: lighten($color1, 20%); + background: $color1; text-align: center; padding: 20px; - padding-top: 100px; font-size: 15px; font-weight: 400; cursor: default; + display: flex; + flex: 1 1 auto; + align-items: center; a { color: $color4; @@ -1395,7 +1433,7 @@ button.icon-button.active i.fa-retweet { .emoji-dialog { width: 280px; height: 220px; - background: $color2; + background: darken($color3, 10%); box-sizing: border-box; border-radius: 2px; overflow: hidden; @@ -1404,6 +1442,8 @@ button.icon-button.active i.fa-retweet { .emojione { margin: 0; + width: 100%; + height: auto; } .emoji-dialog-header { diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 7fd43489f..04e7ddacf 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -2,30 +2,25 @@ class AboutController < ApplicationController before_action :set_body_classes + before_action :set_instance_presenter, only: [:show, :more] - def index - @description = Setting.site_description - @open_registrations = Setting.open_registrations - @closed_registrations_message = Setting.closed_registrations_message + def show; end - @user = User.new - @user.build_account - end - - def more - @description = Setting.site_description - @extended_description = Setting.site_extended_description - @contact_account = Account.find_local(Setting.site_contact_username) - @contact_email = Setting.site_contact_email - @user_count = Rails.cache.fetch('user_count') { User.count } - @status_count = Rails.cache.fetch('local_status_count') { Status.local.count } - @domain_count = Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) } - end + def more; end def terms; end private + def new_user + User.new.tap(&:build_account) + end + helper_method :new_user + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end + def set_body_classes @body_classes = 'about-body' end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 619c04be2..d4f157614 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -35,11 +35,11 @@ class AccountsController < ApplicationController end def followers - @followers = @account.followers.order('follows.created_at desc').paginate(page: params[:page], per_page: 12) + @followers = @account.followers.order('follows.created_at desc').page(params[:page]).per(12) end def following - @following = @account.following.order('follows.created_at desc').paginate(page: params[:page], per_page: 12) + @following = @account.following.order('follows.created_at desc').page(params[:page]).per(12) end private @@ -53,7 +53,7 @@ class AccountsController < ApplicationController end def webfinger_account_url - webfinger_url(resource: "acct:#{@account.acct}@#{Rails.configuration.x.local_domain}") + webfinger_url(resource: @account.to_webfinger_s) end def check_account_suspension diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index df2c7bebf..71cb8edd8 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -1,51 +1,50 @@ # frozen_string_literal: true -class Admin::AccountsController < ApplicationController - before_action :require_admin! - before_action :set_account, except: :index - - layout 'admin' - - def index - @accounts = Account.alphabetic.paginate(page: params[:page], per_page: 40) - - @accounts = @accounts.local if params[:local].present? - @accounts = @accounts.remote if params[:remote].present? - @accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present? - @accounts = @accounts.silenced if params[:silenced].present? - @accounts = @accounts.recent if params[:recent].present? - @accounts = @accounts.suspended if params[:suspended].present? - end - - def show; end - - def suspend - Admin::SuspensionWorker.perform_async(@account.id) - redirect_to admin_accounts_path - end - - def unsuspend - @account.update(suspended: false) - redirect_to admin_accounts_path - end - - def silence - @account.update(silenced: true) - redirect_to admin_accounts_path - end - - def unsilence - @account.update(silenced: false) - redirect_to admin_accounts_path - end - - private - - def set_account - @account = Account.find(params[:id]) - end - - def account_params - params.require(:account).permit(:silenced, :suspended) +module Admin + class AccountsController < BaseController + before_action :set_account, except: :index + + def index + @accounts = Account.alphabetic.page(params[:page]) + + @accounts = @accounts.local if params[:local].present? + @accounts = @accounts.remote if params[:remote].present? + @accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present? + @accounts = @accounts.silenced if params[:silenced].present? + @accounts = @accounts.recent if params[:recent].present? + @accounts = @accounts.suspended if params[:suspended].present? + end + + def show; end + + def suspend + Admin::SuspensionWorker.perform_async(@account.id) + redirect_to admin_accounts_path + end + + def unsuspend + @account.update(suspended: false) + redirect_to admin_accounts_path + end + + def silence + @account.update(silenced: true) + redirect_to admin_accounts_path + end + + def unsilence + @account.update(silenced: false) + redirect_to admin_accounts_path + end + + private + + def set_account + @account = Account.find(params[:id]) + end + + def account_params + params.require(:account).permit(:silenced, :suspended) + end end end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb new file mode 100644 index 000000000..11fe326bc --- /dev/null +++ b/app/controllers/admin/base_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Admin + class BaseController < ApplicationController + before_action :require_admin! + + layout 'admin' + end +end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 1f4432847..a8b56c085 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -1,32 +1,30 @@ # frozen_string_literal: true -class Admin::DomainBlocksController < ApplicationController - before_action :require_admin! - - layout 'admin' - - def index - @blocks = DomainBlock.paginate(page: params[:page], per_page: 40) - end +module Admin + class DomainBlocksController < BaseController + def index + @blocks = DomainBlock.page(params[:page]) + end - def new - @domain_block = DomainBlock.new - end + def new + @domain_block = DomainBlock.new + end - def create - @domain_block = DomainBlock.new(resource_params) + def create + @domain_block = DomainBlock.new(resource_params) - if @domain_block.save - DomainBlockWorker.perform_async(@domain_block.id) - redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed' - else - render action: :new + if @domain_block.save + DomainBlockWorker.perform_async(@domain_block.id) + redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed' + else + render action: :new + end end - end - private + private - def resource_params - params.require(:domain_block).permit(:domain, :severity) + def resource_params + params.require(:domain_block).permit(:domain, :severity) + end end end diff --git a/app/controllers/admin/pubsubhubbub_controller.rb b/app/controllers/admin/pubsubhubbub_controller.rb index b9e840ffe..31c80a174 100644 --- a/app/controllers/admin/pubsubhubbub_controller.rb +++ b/app/controllers/admin/pubsubhubbub_controller.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -class Admin::PubsubhubbubController < ApplicationController - before_action :require_admin! - - layout 'admin' - - def index - @subscriptions = Subscription.order('id desc').includes(:account).paginate(page: params[:page], per_page: 40) +module Admin + class PubsubhubbubController < BaseController + def index + @subscriptions = Subscription.order('id desc').includes(:account).page(params[:page]) + end end end diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 2b3b1809f..3c3082318 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -1,45 +1,44 @@ # frozen_string_literal: true -class Admin::ReportsController < ApplicationController - before_action :require_admin! - before_action :set_report, except: [:index] - - layout 'admin' - - def index - @reports = Report.includes(:account, :target_account).order('id desc').paginate(page: params[:page], per_page: 40) - @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved - end - - def show - @statuses = Status.where(id: @report.status_ids) - end - - def resolve - @report.update(action_taken: true, action_taken_by_account_id: current_account.id) - redirect_to admin_report_path(@report) - end - - def suspend - Admin::SuspensionWorker.perform_async(@report.target_account.id) - Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id) - redirect_to admin_report_path(@report) - end - - def silence - @report.target_account.update(silenced: true) - Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id) - redirect_to admin_report_path(@report) - end - - def remove - RemovalWorker.perform_async(params[:status_id]) - redirect_to admin_report_path(@report) - end - - private - - def set_report - @report = Report.find(params[:id]) +module Admin + class ReportsController < BaseController + before_action :set_report, except: [:index] + + def index + @reports = Report.includes(:account, :target_account).order('id desc').page(params[:page]) + @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved + end + + def show + @statuses = Status.where(id: @report.status_ids) + end + + def resolve + @report.update(action_taken: true, action_taken_by_account_id: current_account.id) + redirect_to admin_report_path(@report) + end + + def suspend + Admin::SuspensionWorker.perform_async(@report.target_account.id) + Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id) + redirect_to admin_report_path(@report) + end + + def silence + @report.target_account.update(silenced: true) + Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id) + redirect_to admin_report_path(@report) + end + + def remove + RemovalWorker.perform_async(params[:status_id]) + redirect_to admin_report_path(@report) + end + + private + + def set_report + @report = Report.find(params[:id]) + end end end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 7615c781d..6cca5c3e3 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -1,35 +1,33 @@ # frozen_string_literal: true -class Admin::SettingsController < ApplicationController - before_action :require_admin! - - layout 'admin' +module Admin + class SettingsController < BaseController + def index + @settings = Setting.all_as_records + end - def index - @settings = Setting.all_as_records - end + def update + @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id]) + value = settings_params[:value] - def update - @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id]) - value = settings_params[:value] + # Special cases + value = value == 'true' if @setting.var == 'open_registrations' - # Special cases - value = value == 'true' if @setting.var == 'open_registrations' + if @setting.value != value + @setting.value = value + @setting.save + end - if @setting.value != value - @setting.value = value - @setting.save + respond_to do |format| + format.html { redirect_to admin_settings_path } + format.json { respond_with_bip(@setting) } + end end - respond_to do |format| - format.html { redirect_to admin_settings_path } - format.json { respond_with_bip(@setting) } - end - end - - private + private - def settings_params - params.require(:setting).permit(:value) + def settings_params + params.require(:setting).permit(:value) + end end end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 454873116..2c44e36a7 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true class Api::V1::AccountsController < ApiController - before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute] + before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute, :update_credentials] before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute] + before_action -> { doorkeeper_authorize! :write }, only: [:update_credentials] before_action :require_user!, except: [:show, :following, :followers, :statuses] - before_action :set_account, except: [:verify_credentials, :suggestions, :search] + before_action :set_account, except: [:verify_credentials, :update_credentials, :suggestions, :search] respond_to :json @@ -15,6 +16,12 @@ class Api::V1::AccountsController < ApiController render action: :show end + def update_credentials + current_account.update!(account_params) + @account = current_account + render action: :show + end + def following results = Follow.where(account: @account).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) accounts = Account.where(id: results.map(&:target_account_id)).map { |a| [a.id, a] }.to_h @@ -135,4 +142,8 @@ class Api::V1::AccountsController < ApiController def statuses_pagination_params(core_params) params.permit(:limit, :only_media, :exclude_replies).merge(core_params) end + + def account_params + params.permit(:display_name, :note, :avatar, :header) + end end diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 71c054334..3cff29982 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -9,7 +9,7 @@ class Api::V1::NotificationsController < ApiController DEFAULT_NOTIFICATIONS_LIMIT = 15 def index - @notifications = Notification.where(account: current_account).browserable.paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id]) + @notifications = Notification.where(account: current_account).browserable(exclude_types).paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id]) @notifications = cache_collection(@notifications, Notification) statuses = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status) @@ -32,7 +32,13 @@ class Api::V1::NotificationsController < ApiController private + def exclude_types + val = params.permit(exclude_types: [])[:exclude_types] || [] + val = [val] unless val.is_a?(Enumerable) + val + end + def pagination_params(core_params) - params.permit(:limit).merge(core_params) + params.permit(:limit, exclude_types: []).merge(core_params) end end diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index db16f82e5..57604f1dc 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -7,6 +7,7 @@ class ApiController < ApplicationController protect_from_forgery with: :null_session skip_before_action :verify_authenticity_token + skip_before_action :store_current_location before_action :set_rate_limit_headers diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb index 6528ce45e..bcf3fd0a0 100644 --- a/app/controllers/concerns/localized.rb +++ b/app/controllers/concerns/localized.rb @@ -26,6 +26,8 @@ module Localized end def default_locale - ENV.fetch('DEFAULT_LOCALE') { I18n.default_locale } + ENV.fetch('DEFAULT_LOCALE') { + http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale + } end end diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb index 1e3f786ec..22e376836 100644 --- a/app/controllers/remote_follow_controller.rb +++ b/app/controllers/remote_follow_controller.rb @@ -25,7 +25,7 @@ class RemoteFollowController < ApplicationController session[:remote_follow] = @remote_follow.acct - redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s + redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: @account.to_webfinger_s).to_s else render :new end diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 4fcec5322..ff688978c 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -39,7 +39,7 @@ class Settings::ExportsController < ApplicationController def accounts_list_to_csv(list) CSV.generate do |csv| list.each do |account| - csv << [(account.local? ? "#{account.username}@#{Rails.configuration.x.local_domain}" : account.acct)] + csv << [(account.local? ? account.local_username_and_domain : account.acct)] end end end diff --git a/app/controllers/xrd_controller.rb b/app/controllers/xrd_controller.rb index 6db87cefc..5964172e9 100644 --- a/app/controllers/xrd_controller.rb +++ b/app/controllers/xrd_controller.rb @@ -14,7 +14,7 @@ class XrdController < ApplicationController def webfinger @account = Account.find_local!(username_from_resource) - @canonical_account_uri = "acct:#{@account.username}@#{Rails.configuration.x.local_domain}" + @canonical_account_uri = @account.to_webfinger_s @magic_key = pem_to_magic_key(@account.keypair.public_key) respond_to do |format| diff --git a/app/helpers/about_helper.rb b/app/helpers/about_helper.rb deleted file mode 100644 index 0f57a7b5e..000000000 --- a/app/helpers/about_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module AboutHelper -end diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb deleted file mode 100644 index af23a78d1..000000000 --- a/app/helpers/accounts_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module AccountsHelper - def pagination_options - { - previous_label: safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '), - next_label: safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), - inner_window: 1, - outer_window: 0, - } - end -end diff --git a/app/helpers/admin/domain_blocks_helper.rb b/app/helpers/admin/domain_blocks_helper.rb deleted file mode 100644 index d66c8d5e1..000000000 --- a/app/helpers/admin/domain_blocks_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module Admin::DomainBlocksHelper -end diff --git a/app/helpers/admin/pubsubhubbub_helper.rb b/app/helpers/admin/pubsubhubbub_helper.rb deleted file mode 100644 index c2fc2e7da..000000000 --- a/app/helpers/admin/pubsubhubbub_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module Admin::PubsubhubbubHelper -end diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb index b750eeb07..185388ec9 100644 --- a/app/helpers/atom_builder_helper.rb +++ b/app/helpers/atom_builder_helper.rb @@ -160,7 +160,7 @@ module AtomBuilderHelper object_type xml, :person uri xml, TagManager.instance.uri_for(account) name xml, account.username - email xml, account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct + email xml, account.local? ? account.local_username_and_domain : account.acct summary xml, account.note link_alternate xml, TagManager.instance.url_for(account) link_avatar xml, account diff --git a/app/helpers/authorize_follow_helper.rb b/app/helpers/authorize_follow_helper.rb deleted file mode 100644 index 99ee03c2f..000000000 --- a/app/helpers/authorize_follow_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module AuthorizeFollowHelper -end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 74dc0e11d..327ca4e98 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -5,13 +5,15 @@ module SettingsHelper en: 'English', de: 'Deutsch', es: 'Español', + eo: 'Esperanto', pt: 'Português', fr: 'Français', hu: 'Magyar', uk: 'Українська', 'zh-CN': '简体中文', fi: 'Suomi', - eo: 'Esperanto', + ru: 'Русский', + }.freeze def human_locale(locale) diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb deleted file mode 100644 index 5b2b3ca59..000000000 --- a/app/helpers/tags_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module TagsHelper -end diff --git a/app/helpers/xrd_helper.rb b/app/helpers/xrd_helper.rb deleted file mode 100644 index 2281a0278..000000000 --- a/app/helpers/xrd_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module XrdHelper -end diff --git a/app/lib/atom_serializer.rb b/app/lib/atom_serializer.rb index b9dcee6b3..68d2fce68 100644 --- a/app/lib/atom_serializer.rb +++ b/app/lib/atom_serializer.rb @@ -20,7 +20,7 @@ class AtomSerializer append_element(author, 'activity:object-type', TagManager::TYPES[:person]) append_element(author, 'uri', uri) append_element(author, 'name', account.username) - append_element(author, 'email', account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct) + append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct) append_element(author, 'summary', account.note) append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account)) append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original))) @@ -67,7 +67,7 @@ class AtomSerializer append_element(entry, 'id', TagManager.instance.unique_tag(stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type)) append_element(entry, 'published', stream_entry.created_at.iso8601) append_element(entry, 'updated', stream_entry.updated_at.iso8601) - append_element(entry, 'title', stream_entry&.status&.title) + append_element(entry, 'title', stream_entry&.status&.title || 'Delete') entry << author(stream_entry.account) if root diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 58d9fb1fc..339a5c78b 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -66,7 +66,7 @@ class FeedManager timeline_key = key(:home, into_account.id) oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0 - from_account.statuses.select('id').where('id > ?', oldest_home_score).find_in_batches do |statuses| + from_account.statuses.select('id').where('id > ?', oldest_home_score).reorder(nil).find_in_batches do |statuses| redis.pipelined do statuses.each do |status| redis.zrem(timeline_key, status.id) diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index da7ad2027..c3f331ff7 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -15,7 +15,6 @@ class Formatter html = status.text html = encode(html) html = simple_format(html, {}, sanitize: false) - html = html.gsub(/\n/, '') html = link_urls(html) html = link_mentions(html, status.mentions) html = link_hashtags(html) diff --git a/app/models/account.rb b/app/models/account.rb index cbba8b5b6..8ceda7f97 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -12,12 +12,12 @@ class Account < ApplicationRecord validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?' # Avatar upload - has_attached_file :avatar, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' } + has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-quality 80 -strip' } validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES validates_attachment_size :avatar, less_than: 2.megabytes # Header upload - has_attached_file :header, styles: { original: '700x335#' }, convert_options: { all: '-quality 80 -strip' } + has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-quality 80 -strip' } validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES validates_attachment_size :header, less_than: 2.megabytes @@ -120,6 +120,14 @@ class Account < ApplicationRecord local? ? username : "#{username}@#{domain}" end + def local_username_and_domain + "#{username}@#{Rails.configuration.x.local_domain}" + end + + def to_webfinger_s + "acct:#{local_username_and_domain}" + end + def subscribed? !subscription_expires_at.blank? end @@ -150,6 +158,22 @@ class Account < ApplicationRecord save! end + def avatar_original_url + avatar.url(:original) + end + + def avatar_static_url + avatar_content_type == 'image/gif' ? avatar.url(:static) : avatar_original_url + end + + def header_original_url + header.url(:original) + end + + def header_static_url + header_content_type == 'image/gif' ? header.url(:static) : header_original_url + end + def avatar_remote_url=(url) parsed_url = URI.parse(url) @@ -203,7 +227,7 @@ class Account < ApplicationRecord end def triadic_closures(account, limit = 5) - sql = <<SQL + sql = <<-SQL.squish WITH first_degree AS ( SELECT target_account_id FROM follows @@ -216,7 +240,7 @@ class Account < ApplicationRecord GROUP BY target_account_id, accounts.id ORDER BY count(account_id) DESC LIMIT ? -SQL + SQL Account.find_by_sql([sql, account.id, account.id, limit]) end @@ -226,7 +250,7 @@ SQL textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))' query = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')' - sql = <<SQL + sql = <<-SQL.squish SELECT accounts.*, ts_rank_cd(#{textsearch}, #{query}, 32) AS rank @@ -234,7 +258,7 @@ SQL WHERE #{query} @@ #{textsearch} ORDER BY rank DESC LIMIT ? -SQL + SQL Account.find_by_sql([sql, limit]) end @@ -244,7 +268,7 @@ SQL textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))' query = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')' - sql = <<SQL + sql = <<-SQL.squish SELECT accounts.*, (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank @@ -254,7 +278,7 @@ SQL GROUP BY accounts.id ORDER BY rank DESC LIMIT ? -SQL + SQL Account.find_by_sql([sql, account.id, account.id, limit]) end @@ -284,6 +308,18 @@ SQL def follow_mapping(query, field) query.pluck(field).inject({}) { |mapping, id| mapping[id] = true; mapping } end + + def avatar_styles(file) + styles = { original: '120x120#' } + styles[:static] = { format: 'png' } if file.content_type == 'image/gif' + styles + end + + def header_styles(file) + styles = { original: '700x335#' } + styles[:static] = { format: 'png' } if file.content_type == 'image/gif' + styles + end end before_create do diff --git a/app/models/notification.rb b/app/models/notification.rb index b7b474869..302d4382d 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -16,10 +16,17 @@ class Notification < ApplicationRecord validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] } + TYPE_CLASS_MAP = { + mention: 'Mention', + reblog: 'Status', + follow: 'Follow', + follow_request: 'FollowRequest', + favourite: 'Favourite', + }.freeze + STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze scope :cache_ids, -> { select(:id, :updated_at, :activity_type, :activity_id) } - scope :browserable, -> { where.not(activity_type: ['FollowRequest']) } cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account @@ -28,12 +35,7 @@ class Notification < ApplicationRecord end def type - case activity_type - when 'Status' - :reblog - else - activity_type.underscore.to_sym - end + @type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym end def target_status @@ -50,6 +52,11 @@ class Notification < ApplicationRecord end class << self + def browserable(types = []) + types.concat([:follow_request]) + where.not(activity_type: activity_types_from_types(types)) + end + def reload_stale_associations!(cached_items) account_ids = cached_items.map(&:from_account_id).uniq accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h @@ -58,6 +65,12 @@ class Notification < ApplicationRecord item.from_account = accounts[item.from_account_id] end end + + private + + def activity_types_from_types(types) + types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact + end end after_initialize :set_from_account diff --git a/app/models/status.rb b/app/models/status.rb index 7e3dd3e28..16cd4383f 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -75,7 +75,7 @@ class Status < ApplicationRecord end def title - content + reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}" end def hidden? diff --git a/app/models/tag.rb b/app/models/tag.rb index 15625ca43..6209d7dab 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -17,7 +17,7 @@ class Tag < ApplicationRecord textsearch = 'to_tsvector(\'simple\', tags.name)' query = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')' - sql = <<SQL + sql = <<-SQL.squish SELECT tags.*, ts_rank_cd(#{textsearch}, #{query}) AS rank @@ -25,7 +25,7 @@ class Tag < ApplicationRecord WHERE #{query} @@ #{textsearch} ORDER BY rank DESC LIMIT ? -SQL + SQL Tag.find_by_sql([sql, limit]) end diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb new file mode 100644 index 000000000..cd809566f --- /dev/null +++ b/app/presenters/instance_presenter.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class InstancePresenter + delegate( + :closed_registrations_message, + :contact_email, + :open_registrations, + :site_description, + :site_extended_description, + to: Setting + ) + + def contact_account + Account.find_local(Setting.site_contact_username) + end + + def user_count + Rails.cache.fetch('user_count') { User.count } + end + + def status_count + Rails.cache.fetch('local_status_count') { Status.local.count } + end + + def domain_count + Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) } + end +end diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb index 6a6a696d6..50ffc47c6 100644 --- a/app/services/fetch_remote_account_service.rb +++ b/app/services/fetch_remote_account_service.rb @@ -19,11 +19,16 @@ class FetchRemoteAccountService < BaseService xml = Nokogiri::XML(body) xml.encoding = 'utf-8' - url_parts = Addressable::URI.parse(url) - username = xml.at_xpath('//xmlns:author/xmlns:name').try(:content) - domain = url_parts.host + email = xml.at_xpath('//xmlns:author/xmlns:email').try(:content) + if email.nil? + url_parts = Addressable::URI.parse(url) + username = xml.at_xpath('//xmlns:author/xmlns:name').try(:content) + domain = url_parts.host + else + username, domain = email.split('@') + end - return nil if username.nil? + return nil if username.nil? || domain.nil? Rails.logger.debug "Going to webfinger #{username}@#{domain}" diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml new file mode 100644 index 000000000..c7a9a488b --- /dev/null +++ b/app/views/about/_registration.html.haml @@ -0,0 +1,30 @@ += simple_form_for(new_user, url: user_registration_path) do |f| + = f.simple_fields_for :account do |account_fields| + = account_fields.input :username, + autofocus: true, + placeholder: t('simple_form.labels.defaults.username'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } + + = f.input :email, + placeholder: t('simple_form.labels.defaults.email'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } + = f.input :password, + autocomplete: "off", + placeholder: t('simple_form.labels.defaults.password'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } + = f.input :password_confirmation, + autocomplete: "off", + placeholder: t('simple_form.labels.defaults.confirm_password'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } + + .actions + = f.button :button, t('about.get_started'), type: :submit + + .info + = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' + · + = link_to t('about.about_this'), about_more_path diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml index 2de3bf986..8c12f57c1 100644 --- a/app/views/about/more.html.haml +++ b/app/views/about/more.html.haml @@ -7,42 +7,42 @@ .panel %h2= Rails.configuration.x.local_domain - - unless @description.blank? - %p= @description.html_safe + - unless @instance_presenter.site_description.blank? + %p= @instance_presenter.site_description.html_safe .information-board .section %span= t 'about.user_count_before' - %strong= number_with_delimiter @user_count + %strong= number_with_delimiter @instance_presenter.user_count %span= t 'about.user_count_after' .section %span= t 'about.status_count_before' - %strong= number_with_delimiter @status_count + %strong= number_with_delimiter @instance_presenter.status_count %span= t 'about.status_count_after' .section %span= t 'about.domain_count_before' - %strong= number_with_delimiter @domain_count + %strong= number_with_delimiter @instance_presenter.domain_count %span= t 'about.domain_count_after' - - unless @extended_description.blank? - .panel= @extended_description.html_safe + - unless @instance_presenter.site_extended_description.blank? + .panel= @instance_presenter.site_extended_description.html_safe .sidebar .panel .panel-header= t 'about.contact' .panel-body - - if @contact_account + - if @instance_presenter.contact_account .owner - .avatar= image_tag @contact_account.avatar.url + .avatar= image_tag @instance_presenter.contact_account.avatar.url .name - = link_to TagManager.instance.url_for(@contact_account) do - %span.display_name.emojify= display_name(@contact_account) - %span.username= "@#{@contact_account.acct}" + = link_to TagManager.instance.url_for(@instance_presenter.contact_account) do + %span.display_name.emojify= display_name(@instance_presenter.contact_account) + %span.username= "@#{@instance_presenter.contact_account.acct}" - - unless @contact_email.blank? + - unless @instance_presenter.contact_email.blank? .contact-email = t 'about.business_email' - %strong= @contact_email + %strong= @instance_presenter.contact_email .panel .panel-header= t 'about.links' .panel-list diff --git a/app/views/about/index.html.haml b/app/views/about/show.html.haml index f6b0c1668..8a0d00daa 100644 --- a/app/views/about/index.html.haml +++ b/app/views/about/show.html.haml @@ -8,7 +8,7 @@ %meta{ property: 'og:site_name', content: site_title }/ %meta{ property: 'og:type', content: 'website' }/ %meta{ property: 'og:title', content: Rails.configuration.x.local_domain }/ - %meta{ property: 'og:description', content: @description.blank? ? "Mastodon is a free, open-source social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly" : strip_tags(@description) }/ + %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.blank? ? t('about.about_mastodon') : @instance_presenter.site_description) }/ %meta{ property: 'og:image', content: asset_url('mastodon_small.jpg') }/ %meta{ property: 'og:image:width', content: '400' }/ %meta{ property: 'og:image:height', content: '400' }/ @@ -24,28 +24,14 @@ .screenshot-with-signup .mascot= image_tag 'fluffy-elephant-friend.png' - - if @open_registrations - = simple_form_for(@user, url: user_registration_path) do |f| - = f.simple_fields_for :account do |ff| - = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } - - = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } - = f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } - = f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } - - .actions - = f.button :button, t('about.get_started'), type: :submit - - .info - = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' - · - = link_to t('about.about_this'), about_more_path + - if @instance_presenter.open_registrations + = render 'registration' - else .closed-registrations-message - - if @closed_registrations_message.blank? + - if @instance_presenter.closed_registrations_message.blank? %p= t('about.closed_registrations') - else - = @closed_registrations_message.html_safe + = @instance_presenter.closed_registrations_message.html_safe .info = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' · @@ -85,9 +71,9 @@ = fa_icon('li check-square') = t 'about.features.api' - - unless @description.blank? + - unless @instance_presenter.site_description.blank? %h3= t('about.description_headline', domain: Rails.configuration.x.local_domain) - %p= @description.html_safe + %p= @instance_presenter.site_description.html_safe .actions .info diff --git a/app/views/accounts/followers.html.haml b/app/views/accounts/followers.html.haml index 493491020..fa5071f38 100644 --- a/app/views/accounts/followers.html.haml +++ b/app/views/accounts/followers.html.haml @@ -9,4 +9,4 @@ - else = render partial: 'grid_card', collection: @followers, as: :account, cached: true -= will_paginate @followers, pagination_options += paginate @followers diff --git a/app/views/accounts/following.html.haml b/app/views/accounts/following.html.haml index 370cd6c48..987dcba1f 100644 --- a/app/views/accounts/following.html.haml +++ b/app/views/accounts/following.html.haml @@ -9,4 +9,4 @@ - else = render partial: 'grid_card', collection: @following, as: :account, cached: true -= will_paginate @following, pagination_options += paginate @following diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index e90897729..3b0d69dcd 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -31,4 +31,4 @@ .pagination - if @statuses.size == 20 - = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next_page', rel: 'next' + = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next', rel: 'next' diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index f8ed4ef97..4d636601e 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -46,4 +46,4 @@ = table_link_to 'globe', 'Public', TagManager.instance.url_for(account) = table_link_to 'pencil', 'Edit', admin_account_path(account.id) -= will_paginate @accounts, pagination_options += paginate @accounts diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml index eb7894b86..fe6ff683f 100644 --- a/app/views/admin/domain_blocks/index.html.haml +++ b/app/views/admin/domain_blocks/index.html.haml @@ -13,5 +13,5 @@ %samp= block.domain %td= block.severity -= will_paginate @blocks, pagination_options += paginate @blocks = link_to 'Add new', new_admin_domain_block_path, class: 'button' diff --git a/app/views/admin/pubsubhubbub/index.html.haml b/app/views/admin/pubsubhubbub/index.html.haml index cb11a502c..2b8e36e6a 100644 --- a/app/views/admin/pubsubhubbub/index.html.haml +++ b/app/views/admin/pubsubhubbub/index.html.haml @@ -26,4 +26,4 @@ - else = l subscription.last_successful_delivery_at -= will_paginate @subscriptions, pagination_options += paginate @subscriptions diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index 839259dc2..9c5c78935 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -29,4 +29,4 @@ %td= truncate(report.comment, length: 30, separator: ' ') %td= table_link_to 'circle', 'View', admin_report_path(report) -= will_paginate @reports, pagination_options += paginate @reports diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl index 32df0457a..8826aa22d 100644 --- a/app/views/api/v1/accounts/show.rabl +++ b/app/views/api/v1/accounts/show.rabl @@ -4,8 +4,9 @@ attributes :id, :username, :acct, :display_name, :locked, :created_at node(:note) { |account| Formatter.instance.simplified_format(account) } node(:url) { |account| TagManager.instance.url_for(account) } -node(:avatar) { |account| full_asset_url(account.avatar.url(:original)) } -node(:header) { |account| full_asset_url(account.header.url(:original)) } -node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : account.followers_count } -node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : account.following_count } -node(:statuses_count) { |account| defined?(@statuses_counts_map) ? (@statuses_counts_map[account.id] || 0) : account.statuses_count } +node(:avatar) { |account| full_asset_url(account.avatar_original_url) } +node(:avatar_static) { |account| full_asset_url(account.avatar_static_url) } +node(:header) { |account| full_asset_url(account.header_original_url) } +node(:header_static) { |account| full_asset_url(account.header_static_url) } + +attributes :followers_count, :following_count, :statuses_count diff --git a/app/views/kaminari/_next_page.html.haml b/app/views/kaminari/_next_page.html.haml new file mode 100644 index 000000000..30a3643d6 --- /dev/null +++ b/app/views/kaminari/_next_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "Next" page +-# available local variables +-# url: url to the next page +-# current_page: a page object for the currently displayed page +-# total_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.next + = link_to_unless current_page.last?, safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), url, rel: 'next', remote: remote diff --git a/app/views/kaminari/_paginator.html.haml b/app/views/kaminari/_paginator.html.haml new file mode 100644 index 000000000..b1da236d5 --- /dev/null +++ b/app/views/kaminari/_paginator.html.haml @@ -0,0 +1,16 @@ +-# The container tag +-# available local variables +-# current_page: a page object for the currently displayed page +-# total_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +-# paginator: the paginator that renders the pagination tags inside += paginator.render do + %nav.pagination + = prev_page_tag unless current_page.first? + - each_page do |page| + - if page.display_tag? + = page_tag page + - elsif !page.was_truncated? + = gap_tag + = next_page_tag unless current_page.last? diff --git a/app/views/kaminari/_prev_page.html.haml b/app/views/kaminari/_prev_page.html.haml new file mode 100644 index 000000000..1089e3566 --- /dev/null +++ b/app/views/kaminari/_prev_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "Previous" page +-# available local variables +-# url: url to the previous page +-# current_page: a page object for the currently displayed page +-# total_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.prev + = link_to_unless current_page.first?, safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '), url, rel: 'prev', remote: remote diff --git a/app/views/shared/_landing_strip.html.haml b/app/views/shared/_landing_strip.html.haml index bb081e544..3536c5ca8 100644 --- a/app/views/shared/_landing_strip.html.haml +++ b/app/views/shared/_landing_strip.html.haml @@ -1,2 +1,5 @@ .landing-strip - = t('landing_strip_html', name: display_name(account), domain: Rails.configuration.x.local_domain, sign_up_path: new_user_registration_path) + = t('landing_strip_html', + name: content_tag(:span, display_name(account), class: :emojify), + domain: Rails.configuration.x.local_domain, + sign_up_path: new_user_registration_path) diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml index 434c5c8da..1333d4d82 100644 --- a/app/views/stream_entries/_status.html.haml +++ b/app/views/stream_entries/_status.html.haml @@ -13,7 +13,7 @@ = fa_icon('retweet fw') %span = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do - %strong= display_name(status.account) + %strong.emojify= display_name(status.account) = t('stream_entries.reblogged') = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: status.proper } diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml index 32a50e158..c894cdb2e 100644 --- a/app/views/tags/show.html.haml +++ b/app/views/tags/show.html.haml @@ -15,4 +15,4 @@ - 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' + = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next', rel: 'next' diff --git a/app/views/user_mailer/confirmation_instructions.fr.html.erb b/app/views/user_mailer/confirmation_instructions.fr.html.erb index 2665f1a20..6c45f1a21 100644 --- a/app/views/user_mailer/confirmation_instructions.fr.html.erb +++ b/app/views/user_mailer/confirmation_instructions.fr.html.erb @@ -1,5 +1,5 @@ <p>Bienvenue <%= @resource.email %> !</p> -<p>Vous pouvez confirmer l'email de votre compte Mastodon en cliquant sur le lien ci-dessous :</p> +<p>Vous pouvez confirmer le courriel de votre compte Mastodon en cliquant sur le lien ci-dessous :</p> <p><%= link_to 'Confirmer mon compte', confirmation_url(@resource, confirmation_token: @token) %></p> diff --git a/app/views/user_mailer/confirmation_instructions.fr.text.erb b/app/views/user_mailer/confirmation_instructions.fr.text.erb index 9d33450f8..dfa3f9f7c 100644 --- a/app/views/user_mailer/confirmation_instructions.fr.text.erb +++ b/app/views/user_mailer/confirmation_instructions.fr.text.erb @@ -1,5 +1,5 @@ Bienvenue <%= @resource.email %> ! -Vous pouvez confirmer l'email de votre compte Mastodon en cliquant sur le lien ci-dessous : +Vous pouvez confirmer le courriel de votre compte Mastodon en cliquant sur le lien ci-dessous : <%= confirmation_url(@resource, confirmation_token: @token) %> diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb index d5a33cada..ad4f1b004 100644 --- a/app/workers/import_worker.rb +++ b/app/workers/import_worker.rb @@ -25,7 +25,7 @@ class ImportWorker def process_blocks(import) from_account = import.account - CSV.foreach(import.data.path) do |row| + CSV.new(open(import.data.url)).each do |row| next if row.size != 1 begin @@ -41,7 +41,7 @@ class ImportWorker def process_follows(import) from_account = import.account - CSV.foreach(import.data.path) do |row| + CSV.new(open(import.data.url)).each do |row| next if row.size != 1 begin |