diff options
Diffstat (limited to 'app/assets')
21 files changed, 294 insertions, 238 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 { |