about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch
diff options
context:
space:
mode:
authorStarfall <admin@plural.cafe>2020-07-01 12:06:19 -0500
committerStarfall <admin@plural.cafe>2020-07-01 12:06:19 -0500
commit4d93b5c442ff5c9f4d640b4c7d543f0c04c120df (patch)
tree4df391c12dc761ac99ca6421d53d8d31870b68ec /app/javascript/flavours/glitch
parent5668836f56cddf3257f38a2483c1d42cacbad3a8 (diff)
parent39a0622de70dc24275808cee9526658bd68a55ed (diff)
Merge branch 'glitch' into main
Diffstat (limited to 'app/javascript/flavours/glitch')
-rw-r--r--app/javascript/flavours/glitch/actions/account_notes.js69
-rw-r--r--app/javascript/flavours/glitch/features/account/components/account_note.js103
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js11
-rw-r--r--app/javascript/flavours/glitch/features/account/containers/account_note_container.js34
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/components/header.js6
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js5
-rw-r--r--app/javascript/flavours/glitch/features/emoji_picker/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/status/components/card.js15
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/status_list_container.js4
-rw-r--r--app/javascript/flavours/glitch/features/video/index.js33
-rw-r--r--app/javascript/flavours/glitch/reducers/account_notes.js44
-rw-r--r--app/javascript/flavours/glitch/reducers/index.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/markers.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/relationships.js4
-rw-r--r--app/javascript/flavours/glitch/styles/admin.scss18
-rw-r--r--app/javascript/flavours/glitch/styles/basics.scss29
-rw-r--r--app/javascript/flavours/glitch/styles/components/accounts.scss66
-rw-r--r--app/javascript/flavours/glitch/styles/components/media.scss3
-rw-r--r--app/javascript/flavours/glitch/styles/components/status.scss6
-rw-r--r--app/javascript/flavours/glitch/styles/statuses.scss16
20 files changed, 435 insertions, 37 deletions
diff --git a/app/javascript/flavours/glitch/actions/account_notes.js b/app/javascript/flavours/glitch/actions/account_notes.js
new file mode 100644
index 000000000..c1cce3193
--- /dev/null
+++ b/app/javascript/flavours/glitch/actions/account_notes.js
@@ -0,0 +1,69 @@
+import api from 'flavours/glitch/util/api';
+
+export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
+export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
+export const ACCOUNT_NOTE_SUBMIT_FAIL    = 'ACCOUNT_NOTE_SUBMIT_FAIL';
+
+export const ACCOUNT_NOTE_INIT_EDIT = 'ACCOUNT_NOTE_INIT_EDIT';
+export const ACCOUNT_NOTE_CANCEL    = 'ACCOUNT_NOTE_CANCEL';
+
+export const ACCOUNT_NOTE_CHANGE_COMMENT = 'ACCOUNT_NOTE_CHANGE_COMMENT';
+
+export function submitAccountNote() {
+  return (dispatch, getState) => {
+    dispatch(submitAccountNoteRequest());
+
+    const id = getState().getIn(['account_notes', 'edit', 'account_id']);
+
+    api(getState).post(`/api/v1/accounts/${id}/note`, {
+      comment: getState().getIn(['account_notes', 'edit', 'comment']),
+    }).then(response => {
+      dispatch(submitAccountNoteSuccess(response.data));
+    }).catch(error => dispatch(submitAccountNoteFail(error)));
+  };
+};
+
+export function submitAccountNoteRequest() {
+  return {
+    type: ACCOUNT_NOTE_SUBMIT_REQUEST,
+  };
+};
+
+export function submitAccountNoteSuccess(relationship) {
+  return {
+    type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
+    relationship,
+  };
+};
+
+export function submitAccountNoteFail(error) {
+  return {
+    type: ACCOUNT_NOTE_SUBMIT_FAIL,
+    error,
+  };
+};
+
+export function initEditAccountNote(account) {
+  return (dispatch, getState) => {
+    const comment = getState().getIn(['relationships', account.get('id'), 'note']);
+
+    dispatch({
+      type: ACCOUNT_NOTE_INIT_EDIT,
+      account,
+      comment,
+    });
+  };
+};
+
+export function cancelAccountNote() {
+  return {
+    type: ACCOUNT_NOTE_CANCEL,
+  };
+};
+
+export function changeAccountNoteComment(comment) {
+  return {
+    type: ACCOUNT_NOTE_CHANGE_COMMENT,
+    comment,
+  };
+};
diff --git a/app/javascript/flavours/glitch/features/account/components/account_note.js b/app/javascript/flavours/glitch/features/account/components/account_note.js
new file mode 100644
index 000000000..e7fd4c5ff
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/account/components/account_note.js
@@ -0,0 +1,103 @@
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import Icon from 'flavours/glitch/components/icon';
+import Textarea from 'react-textarea-autosize';
+
+const messages = defineMessages({
+  placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' },
+});
+
+export default @injectIntl
+class Header extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.map.isRequired,
+    isEditing: PropTypes.bool,
+    isSubmitting: PropTypes.bool,
+    accountNote: PropTypes.string,
+    onEditAccountNote: PropTypes.func.isRequired,
+    onCancelAccountNote: PropTypes.func.isRequired,
+    onSaveAccountNote: PropTypes.func.isRequired,
+    onChangeAccountNote: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleChangeAccountNote = (e) => {
+    this.props.onChangeAccountNote(e.target.value);
+  };
+
+  componentWillUnmount () {
+    if (this.props.isEditing) {
+      this.props.onCancelAccountNote();
+    }
+  }
+
+  handleKeyDown = e => {
+    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
+      this.props.onSaveAccountNote();
+    } else if (e.keyCode === 27) {
+      this.props.onCancelAccountNote();
+    }
+  }
+
+  render () {
+    const { account, accountNote, isEditing, isSubmitting, intl } = this.props;
+
+    if (!account || (!accountNote && !isEditing)) {
+      return null;
+    }
+
+    let action_buttons = null;
+    if (isEditing) {
+      action_buttons = (
+        <div className='account__header__account-note__buttons'>
+          <button className='text-btn' tabIndex='0' onClick={this.props.onCancelAccountNote} disabled={isSubmitting}>
+            <Icon id='times' size={15} /> <FormattedMessage id='account_note.cancel' defaultMessage='Cancel' />
+          </button>
+          <div className='flex-spacer' />
+          <button className='text-btn' tabIndex='0' onClick={this.props.onSaveAccountNote} disabled={isSubmitting}>
+            <Icon id='check' size={15} /> <FormattedMessage id='account_note.save' defaultMessage='Save' />
+          </button>
+        </div>
+      );
+    }
+
+    let note_container = null;
+    if (isEditing) {
+      note_container = (
+        <Textarea
+          className='account__header__account-note__content'
+          disabled={isSubmitting}
+          placeholder={intl.formatMessage(messages.placeholder)}
+          value={accountNote}
+          onChange={this.handleChangeAccountNote}
+          onKeyDown={this.handleKeyDown}
+          autoFocus
+        />
+      );
+    } else {
+      note_container = (<div className='account__header__account-note__content'>{accountNote}</div>);
+    }
+
+    return (
+      <div className='account__header__account-note'>
+        <div className='account__header__account-note__header'>
+          <strong><FormattedMessage id='account.account_note_header' defaultMessage='Your note for @{name}' values={{ name: account.get('username') }} /></strong>
+          {!isEditing && (
+            <div>
+              <button className='text-btn' tabIndex='0' onClick={this.props.onEditAccountNote} disabled={isSubmitting}>
+                <Icon id='pencil' size={15} /> <FormattedMessage id='account_note.edit' defaultMessage='Edit' />
+              </button>
+            </div>
+          )}
+        </div>
+        {note_container}
+        {action_buttons}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index c7b54649c..a5abf38ae 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -12,6 +12,7 @@ import Button from 'flavours/glitch/components/button';
 import { shortNumberFormat } from 'flavours/glitch/util/numbers';
 import { NavLink } from 'react-router-dom';
 import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
+import AccountNoteContainer from '../containers/account_note_container';
 
 const messages = defineMessages({
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@@ -46,6 +47,7 @@ const messages = defineMessages({
   unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
   add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
   admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
+  add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' },
 });
 
 const dateFormatOptions = {
@@ -65,6 +67,7 @@ class Header extends ImmutablePureComponent {
     identity_props: ImmutablePropTypes.list,
     onFollow: PropTypes.func.isRequired,
     onBlock: PropTypes.func.isRequired,
+    onEditAccountNote: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
     domain: PropTypes.string.isRequired,
   };
@@ -121,6 +124,8 @@ class Header extends ImmutablePureComponent {
       return null;
     }
 
+    const accountNote = account.getIn(['relationship', 'note']);
+
     let info        = [];
     let actionBtn   = '';
     let lockedIcon  = '';
@@ -172,6 +177,10 @@ class Header extends ImmutablePureComponent {
       menu.push(null);
     }
 
+    if (accountNote === null) {
+      menu.push({ text: intl.formatMessage(messages.add_account_note, { name: account.get('username') }), action: this.props.onEditAccountNote });
+    }
+
     if (account.get('id') === me) {
       if (profileLink) menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink });
       if (preferencesLink) menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink });
@@ -278,6 +287,8 @@ class Header extends ImmutablePureComponent {
             </h1>
           </div>
 
+          <AccountNoteContainer account={account} />
+
           <div className='account__header__extra'>
             <div className='account__header__bio'>
               { (fields.size > 0 || identity_proofs.size > 0) && (
diff --git a/app/javascript/flavours/glitch/features/account/containers/account_note_container.js b/app/javascript/flavours/glitch/features/account/containers/account_note_container.js
new file mode 100644
index 000000000..f1d007ecb
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/account/containers/account_note_container.js
@@ -0,0 +1,34 @@
+import { connect } from 'react-redux';
+import { changeAccountNoteComment, submitAccountNote, initEditAccountNote, cancelAccountNote } from 'flavours/glitch/actions/account_notes';
+import AccountNote from '../components/account_note';
+
+const mapStateToProps = (state, { account }) => {
+  const isEditing = state.getIn(['account_notes', 'edit', 'account_id']) === account.get('id');
+
+  return {
+    isSubmitting: state.getIn(['account_notes', 'edit', 'isSubmitting']),
+    accountNote: isEditing ? state.getIn(['account_notes', 'edit', 'comment']) : account.getIn(['relationship', 'note']),
+    isEditing,
+  };
+};
+
+const mapDispatchToProps = (dispatch, { account }) => ({
+
+  onEditAccountNote() {
+    dispatch(initEditAccountNote(account));
+  },
+
+  onSaveAccountNote() {
+    dispatch(submitAccountNote());
+  },
+
+  onCancelAccountNote() {
+    dispatch(cancelAccountNote());
+  },
+
+  onChangeAccountNote(comment) {
+    dispatch(changeAccountNoteComment(comment));
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(AccountNote);
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
index 0faa8a424..1bab05c72 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
@@ -24,6 +24,7 @@ export default class Header extends ImmutablePureComponent {
     onUnblockDomain: PropTypes.func.isRequired,
     onEndorseToggle: PropTypes.func.isRequired,
     onAddToList: PropTypes.func.isRequired,
+    onEditAccountNote: PropTypes.func.isRequired,
     hideTabs: PropTypes.bool,
     domain: PropTypes.string.isRequired,
   };
@@ -84,6 +85,10 @@ export default class Header extends ImmutablePureComponent {
     this.props.onAddToList(this.props.account);
   }
 
+  handleEditAccountNote = () => {
+    this.props.onEditAccountNote(this.props.account);
+  }
+
   render () {
     const { account, hideTabs, identity_proofs } = this.props;
 
@@ -109,6 +114,7 @@ export default class Header extends ImmutablePureComponent {
           onUnblockDomain={this.handleUnblockDomain}
           onEndorseToggle={this.handleEndorseToggle}
           onAddToList={this.handleAddToList}
+          onEditAccountNote={this.handleEditAccountNote}
           domain={this.props.domain}
         />
 
diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
index fff5e097f..225910292 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
@@ -19,6 +19,7 @@ import { initBlockModal } from 'flavours/glitch/actions/blocks';
 import { initReport } from 'flavours/glitch/actions/reports';
 import { openModal } from 'flavours/glitch/actions/modal';
 import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks';
+import { initEditAccountNote } from 'flavours/glitch/actions/account_notes';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { unfollowModal } from 'flavours/glitch/util/initial_state';
 import { List as ImmutableList } from 'immutable';
@@ -106,6 +107,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }
   },
 
+  onEditAccountNote (account) {
+    dispatch(initEditAccountNote(account));
+  },
+
   onBlockDomain (domain) {
     dispatch(openModal('CONFIRM', {
       message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />,
diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js
index 14e5cb94a..d0d9714a8 100644
--- a/app/javascript/flavours/glitch/features/emoji_picker/index.js
+++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js
@@ -283,7 +283,7 @@ class EmojiPickerMenu extends React.PureComponent {
     if (!emoji.native) {
       emoji.native = emoji.colons;
     }
-    if (!event.ctrlKey) {
+    if (!(event.ctrlKey || event.metaKey)) {
       this.props.onClose();
     }
     this.props.onPick(emoji);
diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js
index 03867e03a..d2de225c0 100644
--- a/app/javascript/flavours/glitch/features/status/components/card.js
+++ b/app/javascript/flavours/glitch/features/status/components/card.js
@@ -7,7 +7,6 @@ import punycode from 'punycode';
 import classnames from 'classnames';
 import { decode as decodeIDNA } from 'flavours/glitch/util/idna';
 import Icon from 'flavours/glitch/components/icon';
-import classNames from 'classnames';
 import { useBlurhash } from 'flavours/glitch/util/initial_state';
 import { decode } from 'blurhash';
 
@@ -156,7 +155,9 @@ export default class Card extends React.PureComponent {
     this.setState({ previewLoaded: true });
   }
 
-  handleReveal = () => {
+  handleReveal = e => {
+    e.preventDefault();
+    e.stopPropagation();
     this.setState({ revealed: true });
   }
 
@@ -194,7 +195,7 @@ export default class Card extends React.PureComponent {
     const height      = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
 
     const description = (
-      <div className={classNames('status-card__content', { 'status-card__content--blurred': !revealed })}>
+      <div className='status-card__content'>
         {title}
         {!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
         <span className='status-card__host'>{provider}</span>
@@ -202,7 +203,7 @@ export default class Card extends React.PureComponent {
     );
 
     let embed     = '';
-    let canvas = <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('status-card__image-preview', { 'status-card__image-preview--hidden' : revealed && this.state.previewLoaded })} />;
+    let canvas = <canvas width={32} height={32} ref={this.setCanvasRef} className={classnames('status-card__image-preview', { 'status-card__image-preview--hidden' : revealed && this.state.previewLoaded })} />;
     let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />;
     let spoilerButton = (
       <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
@@ -210,7 +211,7 @@ export default class Card extends React.PureComponent {
       </button>
     );
     spoilerButton = (
-      <div className={classNames('spoiler-button', { 'spoiler-button--minified': revealed })}>
+      <div className={classnames('spoiler-button', { 'spoiler-button--minified': revealed })}>
         {spoilerButton}
       </div>
     );
@@ -244,7 +245,7 @@ export default class Card extends React.PureComponent {
       }
 
       return (
-        <div className={className} ref={this.setRef}>
+        <div className={className} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}>
           {embed}
           {!compact && description}
         </div>
@@ -254,14 +255,12 @@ export default class Card extends React.PureComponent {
         <div className='status-card__image'>
           {canvas}
           {thumbnail}
-          {!revealed && spoilerButton}
         </div>
       );
     } else {
       embed = (
         <div className='status-card__image'>
           <Icon id='file-text' />
-          {!revealed && spoilerButton}
         </div>
       );
     }
diff --git a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js
index c01d0e5bc..bd2d2eb4e 100644
--- a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js
@@ -33,6 +33,8 @@ const makeGetStatusIds = (pending = false) => createSelector([
     const statusForId = statuses.get(id);
     let showStatus    = true;
 
+    if (statusForId.get('account') === me) return true;
+
     if (columnSettings.getIn(['shows', 'reblog']) === false) {
       showStatus = showStatus && statusForId.get('reblog') === null;
     }
@@ -45,7 +47,7 @@ const makeGetStatusIds = (pending = false) => createSelector([
       showStatus = showStatus && statusForId.get('visibility') !== 'direct';
     }
 
-    if (showStatus && regex && statusForId.get('account') !== me) {
+    if (showStatus && regex) {
       const searchIndex = statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'search_index']) : statusForId.get('search_index');
       showStatus = !regex.test(searchIndex);
     }
diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js
index a89d9c8b0..e5b681064 100644
--- a/app/javascript/flavours/glitch/features/video/index.js
+++ b/app/javascript/flavours/glitch/features/video/index.js
@@ -185,15 +185,26 @@ class Video extends React.PureComponent {
 
   handlePlay = () => {
     this.setState({ paused: false });
+    this._updateTime();
   }
 
   handlePause = () => {
     this.setState({ paused: true });
   }
 
+  _updateTime () {
+    requestAnimationFrame(() => {
+      this.handleTimeUpdate();
+
+      if (!this.state.paused) {
+        this._updateTime();
+      }
+    });
+  }
+
   handleTimeUpdate = () => {
     this.setState({
-      currentTime: Math.floor(this.video.currentTime),
+      currentTime: this.video.currentTime,
       duration: Math.floor(this.video.duration),
     });
   }
@@ -231,7 +242,7 @@ class Video extends React.PureComponent {
       this.video.volume = slideamt;
       this.setState({ volume: slideamt });
     }
-  }, 60);
+  }, 15);
 
   handleMouseDown = e => {
     document.addEventListener('mousemove', this.handleMouseMove, true);
@@ -259,13 +270,14 @@ class Video extends React.PureComponent {
 
   handleMouseMove = throttle(e => {
     const { x } = getPointerPosition(this.seek, e);
-    const currentTime = Math.floor(this.video.duration * x);
+    const currentTime = this.video.duration * x;
 
     if (!isNaN(currentTime)) {
-      this.video.currentTime = currentTime;
-      this.setState({ currentTime });
+      this.setState({ currentTime }, () => {
+        this.video.currentTime = currentTime;
+      });
     }
-  }, 60);
+  }, 15);
 
   togglePlay = () => {
     if (this.state.paused) {
@@ -374,8 +386,10 @@ class Video extends React.PureComponent {
   }
 
   handleProgress = () => {
-    if (this.video.buffered.length > 0) {
-      this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 });
+    const lastTimeRange = this.video.buffered.length - 1;
+
+    if (lastTimeRange > -1) {
+      this.setState({ buffer: Math.ceil(this.video.buffered.end(lastTimeRange) / this.video.duration * 100) });
     }
   }
 
@@ -477,7 +491,6 @@ class Video extends React.PureComponent {
           onClick={this.togglePlay}
           onPlay={this.handlePlay}
           onPause={this.handlePause}
-          onTimeUpdate={this.handleTimeUpdate}
           onLoadedData={this.handleLoadedData}
           onProgress={this.handleProgress}
           onVolumeChange={this.handleVolumeChange}
@@ -518,7 +531,7 @@ class Video extends React.PureComponent {
 
               {(detailed || fullscreen) && (
                 <span>
-                  <span className='video-player__time-current'>{formatTime(currentTime)}</span>
+                  <span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span>
                   <span className='video-player__time-sep'>/</span>
                   <span className='video-player__time-total'>{formatTime(duration)}</span>
                 </span>
diff --git a/app/javascript/flavours/glitch/reducers/account_notes.js b/app/javascript/flavours/glitch/reducers/account_notes.js
new file mode 100644
index 000000000..b1cf2e0aa
--- /dev/null
+++ b/app/javascript/flavours/glitch/reducers/account_notes.js
@@ -0,0 +1,44 @@
+import { Map as ImmutableMap } from 'immutable';
+
+import {
+  ACCOUNT_NOTE_INIT_EDIT,
+  ACCOUNT_NOTE_CANCEL,
+  ACCOUNT_NOTE_CHANGE_COMMENT,
+  ACCOUNT_NOTE_SUBMIT_REQUEST,
+  ACCOUNT_NOTE_SUBMIT_FAIL,
+  ACCOUNT_NOTE_SUBMIT_SUCCESS,
+} from '../actions/account_notes';
+
+const initialState = ImmutableMap({
+  edit: ImmutableMap({
+    isSubmitting: false,
+    account_id: null,
+    comment: null,
+  }),
+});
+
+export default function account_notes(state = initialState, action) {
+  switch (action.type) {
+  case ACCOUNT_NOTE_INIT_EDIT:
+    return state.withMutations((state) => {
+      state.setIn(['edit', 'isSubmitting'], false);
+      state.setIn(['edit', 'account_id'], action.account.get('id'));
+      state.setIn(['edit', 'comment'], action.comment);
+    });
+  case ACCOUNT_NOTE_CHANGE_COMMENT:
+    return state.setIn(['edit', 'comment'], action.comment);
+  case ACCOUNT_NOTE_SUBMIT_REQUEST:
+    return state.setIn(['edit', 'isSubmitting'], true);
+  case ACCOUNT_NOTE_SUBMIT_FAIL:
+    return state.setIn(['edit', 'isSubmitting'], false);
+  case ACCOUNT_NOTE_SUBMIT_SUCCESS:
+  case ACCOUNT_NOTE_CANCEL:
+    return state.withMutations((state) => {
+      state.setIn(['edit', 'isSubmitting'], false);
+      state.setIn(['edit', 'account_id'], null);
+      state.setIn(['edit', 'comment'], null);
+    });
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js
index 852abe9dd..cadbd01a3 100644
--- a/app/javascript/flavours/glitch/reducers/index.js
+++ b/app/javascript/flavours/glitch/reducers/index.js
@@ -37,6 +37,7 @@ import identity_proofs from './identity_proofs';
 import trends from './trends';
 import announcements from './announcements';
 import markers from './markers';
+import account_notes from './account_notes';
 
 const reducers = {
   announcements,
@@ -77,6 +78,7 @@ const reducers = {
   polls,
   trends,
   markers,
+  account_notes,
 };
 
 export default combineReducers(reducers);
diff --git a/app/javascript/flavours/glitch/reducers/markers.js b/app/javascript/flavours/glitch/reducers/markers.js
index 2e67be82e..fb1572ff5 100644
--- a/app/javascript/flavours/glitch/reducers/markers.js
+++ b/app/javascript/flavours/glitch/reducers/markers.js
@@ -1,6 +1,6 @@
 import {
   MARKERS_SUBMIT_SUCCESS,
-} from '../actions/notifications';
+} from '../actions/markers';
 
 const initialState = ImmutableMap({
   home: '0',
diff --git a/app/javascript/flavours/glitch/reducers/relationships.js b/app/javascript/flavours/glitch/reducers/relationships.js
index 4652bbc14..dcaeefcae 100644
--- a/app/javascript/flavours/glitch/reducers/relationships.js
+++ b/app/javascript/flavours/glitch/reducers/relationships.js
@@ -13,6 +13,9 @@ import {
   DOMAIN_BLOCK_SUCCESS,
   DOMAIN_UNBLOCK_SUCCESS,
 } from 'flavours/glitch/actions/domain_blocks';
+import {
+  ACCOUNT_NOTE_SUBMIT_SUCCESS,
+} from 'flavours/glitch/actions/account_notes';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 
 const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship));
@@ -45,6 +48,7 @@ export default function relationships(state = initialState, action) {
   case ACCOUNT_UNMUTE_SUCCESS:
   case ACCOUNT_PIN_SUCCESS:
   case ACCOUNT_UNPIN_SUCCESS:
+  case ACCOUNT_NOTE_SUBMIT_SUCCESS:
     return normalizeRelationship(state, action.relationship);
   case RELATIONSHIPS_FETCH_SUCCESS:
     return normalizeRelationships(state, action.relationships);
diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 1c8f2271f..3cf5ee970 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -171,9 +171,7 @@ $content-width: 840px;
   }
 
   .content {
-    padding: 20px 15px;
-    padding-top: 60px;
-    padding-left: 25px;
+    padding: 55px 15px 20px 25px;
 
     @media screen and (max-width: $no-columns-breakpoint) {
       max-width: none;
@@ -184,7 +182,7 @@ $content-width: 840px;
     &-heading {
       display: flex;
 
-      padding-bottom: 40px;
+      padding-bottom: 36px;
       border-bottom: 1px solid lighten($ui-base-color, 8%);
 
       margin: -15px -15px 40px 0;
@@ -215,7 +213,7 @@ $content-width: 840px;
     h2 {
       color: $secondary-text-color;
       font-size: 24px;
-      line-height: 28px;
+      line-height: 36px;
       font-weight: 400;
 
       @media screen and (max-width: $no-columns-breakpoint) {
@@ -528,6 +526,16 @@ body,
   max-width: 100%;
 }
 
+.simple_form {
+  .actions {
+    margin-top: 15px;
+  }
+
+  .button {
+    font-size: 15px;
+  }
+}
+
 .batch-form-box {
   display: flex;
   flex-wrap: wrap;
diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss
index 9ff3f3bac..be0e1b860 100644
--- a/app/javascript/flavours/glitch/styles/basics.scss
+++ b/app/javascript/flavours/glitch/styles/basics.scss
@@ -66,6 +66,35 @@ body {
     }
   }
 
+  &.player {
+    padding: 0;
+    margin: 0;
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+
+    & > div {
+      height: 100%;
+    }
+
+    .video-player video {
+      width: 100%;
+      height: 100%;
+      max-height: 100vh;
+    }
+
+    .media-gallery {
+      margin-top: 0;
+      height: 100% !important;
+      border-radius: 0;
+    }
+
+    .media-gallery__item {
+      border-radius: 0;
+    }
+  }
+
   &.embed {
     background: lighten($ui-base-color, 4%);
     margin: 0;
diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss
index 610e48f92..774254a4c 100644
--- a/app/javascript/flavours/glitch/styles/components/accounts.scss
+++ b/app/javascript/flavours/glitch/styles/components/accounts.scss
@@ -379,7 +379,6 @@
   color: $primary-text-color;
   margin-bottom: 4px;
   display: block;
-  vertical-align: top;
   background-color: $base-overlay-background;
   text-transform: uppercase;
   font-size: 11px;
@@ -605,7 +604,7 @@
   &__tabs {
     display: flex;
     align-items: flex-start;
-    padding: 7px 5px;
+    padding: 7px 10px;
     margin-top: -55px;
 
     &__buttons {
@@ -627,7 +626,7 @@
     }
 
     &__name {
-      padding: 5px;
+      padding: 5px 10px;
 
       .account-role {
         vertical-align: top;
@@ -713,4 +712,65 @@
       }
     }
   }
+
+  &__account-note {
+    margin: 5px;
+    padding: 10px;
+    background: $ui-highlight-color;
+    color: $primary-text-color;
+    display: flex;
+    flex-direction: column;
+    border-radius: 4px;
+    font-size: 14px;
+    font-weight: 400;
+
+    &__header {
+      display: flex;
+      flex-direction: row;
+      justify-content: space-between;
+    }
+
+    &__content {
+      white-space: pre-wrap;
+      margin-top: 5px;
+    }
+
+    &__buttons {
+      display: flex;
+      flex-direction: row;
+      justify-content: flex-end;
+      margin-top: 5px;
+
+      .flex-spacer {
+        flex: 0 0 20px;
+        background: transparent;
+      }
+    }
+
+    strong {
+      font-size: 15px;
+      font-weight: 500;
+    }
+
+    button:hover span {
+      text-decoration: underline;
+    }
+
+    textarea {
+      display: block;
+      box-sizing: border-box;
+      width: 100%;
+      margin: 0;
+      margin-top: 5px;
+      color: $inverted-text-color;
+      background: $simple-background-color;
+      padding: 10px;
+      font-family: inherit;
+      font-size: 14px;
+      resize: none;
+      border: 0;
+      outline: 0;
+      border-radius: 4px;
+    }
+  }
 }
diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss
index dbf0c908d..772b40dc4 100644
--- a/app/javascript/flavours/glitch/styles/components/media.scss
+++ b/app/javascript/flavours/glitch/styles/components/media.scss
@@ -76,7 +76,7 @@
   border-radius: 4px;
   position: relative;
   width: 100%;
-  height: 110px;
+  min-height: 64px;
 
   @include fullwidth-gallery;
 }
@@ -404,6 +404,7 @@
   @include fullwidth-gallery;
 
   video {
+    display: block;
     max-width: 100vw;
     max-height: 80vh;
     z-index: 1;
diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss
index 28a4ce0ce..fe4f16353 100644
--- a/app/javascript/flavours/glitch/styles/components/status.scss
+++ b/app/javascript/flavours/glitch/styles/components/status.scss
@@ -776,6 +776,7 @@ a.status__display-name,
 }
 
 .status-card {
+  position: relative;
   display: flex;
   font-size: 14px;
   border: 1px solid lighten($ui-base-color, 8%);
@@ -874,11 +875,6 @@ a.status-card {
   flex: 1 1 auto;
   overflow: hidden;
   padding: 14px 14px 14px 8px;
-
-  &--blurred {
-    filter: blur(2px);
-    pointer-events: none;
-  }
 }
 
 .status-card__description {
diff --git a/app/javascript/flavours/glitch/styles/statuses.scss b/app/javascript/flavours/glitch/styles/statuses.scss
index 6fcc11e29..a71bb2552 100644
--- a/app/javascript/flavours/glitch/styles/statuses.scss
+++ b/app/javascript/flavours/glitch/styles/statuses.scss
@@ -136,6 +136,11 @@
 
   .detailed-status {
     padding: 15px;
+
+    .detailed-status__display-avatar .account__avatar {
+      width: 48px;
+      height: 48px;
+    }
   }
 
   .status {
@@ -196,7 +201,8 @@
       display: initial;
     }
 
-    .status__relative-time {
+    .status__relative-time,
+    .status__visibility-icon {
       color: $dark-text-color;
       float: right;
       font-size: 14px;
@@ -205,6 +211,11 @@
       padding: initial;
     }
 
+    .status__visibility-icon {
+      margin-left: 4px;
+      margin-right: 4px;
+    }
+
     .status__info .status__display-name {
       display: block;
       max-width: 100%;
@@ -238,7 +249,8 @@
         padding-right: 0;
       }
 
-      .status__relative-time {
+      .status__relative-time,
+      .status__visibility-icon {
         float: left;
       }
     }