about summary refs log tree commit diff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/components/actions/notifications.jsx12
-rw-r--r--app/assets/javascripts/components/components/account.jsx2
-rw-r--r--app/assets/javascripts/components/components/avatar.jsx135
-rw-r--r--app/assets/javascripts/components/components/status.jsx2
-rw-r--r--app/assets/javascripts/components/components/status_content.jsx13
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx6
-rw-r--r--app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx22
-rw-r--r--app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/navigation_bar.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/reply_indicator.jsx2
-rw-r--r--app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx2
-rw-r--r--app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx12
-rw-r--r--app/assets/javascripts/components/features/notifications/components/notification.jsx8
-rw-r--r--app/assets/javascripts/components/features/status/components/detailed_status.jsx2
-rw-r--r--app/assets/javascripts/components/locales/fr.jsx2
-rw-r--r--app/assets/javascripts/components/locales/index.jsx6
-rw-r--r--app/assets/javascripts/components/locales/pt.jsx71
-rw-r--r--app/assets/javascripts/components/locales/ru.jsx68
19 files changed, 193 insertions, 178 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;