Compare commits
176 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7406404fa3 | ||
|
010e4f2879 | ||
|
ebbbcfef1c | ||
|
3d776de2cb | ||
|
831ff60698 | ||
|
c25426ca47 | ||
|
09b93aaf85 | ||
|
d4fedf84e0 | ||
|
2af4f3c4e2 | ||
|
b8e7eee837 | ||
|
b48c9013aa | ||
|
b48f2cbc8b | ||
|
1736badf28 | ||
|
88725d6ce8 | ||
|
be0a01145b | ||
|
1f805a6377 | ||
|
e595ccb294 | ||
|
43a29a9d0f | ||
|
0dd71af5fa | ||
|
0618f09939 | ||
|
a23eaf720e | ||
|
9d3fc1281d | ||
|
8857cabca4 | ||
|
affd75936e | ||
|
3e78b7cc3a | ||
|
647a148d4d | ||
|
bb777c24ff | ||
|
0576daf5f9 | ||
|
c442cade78 | ||
|
6004b143a8 | ||
|
25cbb8454c | ||
|
416c9675fc | ||
|
6a1e287053 | ||
|
5bda32e460 | ||
|
6137268e79 | ||
|
234e931db2 | ||
|
fdcb55a0a6 | ||
|
d97a0525aa | ||
|
193dddb433 | ||
|
8fe36654ef | ||
|
8ee1af9530 | ||
|
e4c294432f | ||
|
3ebaeccec9 | ||
|
7177e37b99 | ||
|
fbc5099402 | ||
|
45b379abac | ||
|
6ae975996e | ||
|
5d26c70a9c | ||
|
322cbf83c8 | ||
|
fc99d11703 | ||
|
48652cb41e | ||
|
d4f7f11c3c | ||
|
1ce951d0be | ||
|
52c119052a | ||
|
9317ec8eb1 | ||
|
1b9447853b | ||
|
122d59ac41 | ||
|
8b5179d006 | ||
|
3ea5b948a4 | ||
|
164bad171f | ||
|
acc691851d | ||
|
d2159deaf2 | ||
|
17c591ffba | ||
|
bb04a9be52 | ||
|
338df98ddf | ||
|
da022e1e4f | ||
|
f098f55cab | ||
|
bc955eaf61 | ||
|
2d99c962df | ||
|
e59f5c8e13 | ||
|
b073b092c9 | ||
|
092fdc89fa | ||
|
d000a0b58a | ||
|
cf845fed38 | ||
|
72c984e105 | ||
|
3d3707a077 | ||
|
a0dd90a397 | ||
|
1e2a5dded7 | ||
|
549ce78cf5 | ||
|
1801a36414 | ||
|
501514960a | ||
|
ef5937da1f | ||
|
072c6f1527 | ||
|
55e32fe579 | ||
|
7ee8e50b9c | ||
|
995f0ad51c | ||
|
948dd26931 | ||
|
55e1e12b7d | ||
|
6e27e08cb4 | ||
|
d670f72830 | ||
|
57cd6546c3 | ||
|
1244630ab4 | ||
|
a9a4710fe1 | ||
|
cc83ee60fb | ||
|
89dc29affb | ||
|
85e09518b9 | ||
|
663f090c45 | ||
|
1cf9e14a41 | ||
|
0c2fe22bc1 | ||
|
6f0b3b069f | ||
|
9cd20a7062 | ||
|
219fb317ee | ||
|
66fd8e7821 | ||
|
83e3538181 | ||
|
723f25a999 | ||
|
59b1de0bcf | ||
|
df46864b39 | ||
|
cca41ea544 | ||
|
532bec6e56 | ||
|
ee82d8a876 | ||
|
1646ca75f0 | ||
|
f876665264 | ||
|
5817bae2da | ||
|
4cfc155560 | ||
|
a1174a6d7e | ||
|
5357329454 | ||
|
7fea36d155 | ||
|
2bf7e81ed5 | ||
|
ca8ae21b52 | ||
|
d0ec4fb828 | ||
|
4428cf6f07 | ||
|
fe43991d02 | ||
|
8565ba68f7 | ||
|
629d35e6f5 | ||
|
67dea31b0f | ||
|
6af21daac9 | ||
|
9d3be5579a | ||
|
05ac28f3e4 | ||
|
0f852c6f74 | ||
|
974ac467de | ||
|
af7d02da5d | ||
|
5abd543766 | ||
|
1948f9e767 | ||
|
27ea2a88c1 | ||
|
16cd648181 | ||
|
53b21ac1cd | ||
|
e4550811b2 | ||
|
704846a258 | ||
|
935aecdc32 | ||
|
1714f08d75 | ||
|
452dc6b5fe | ||
|
5c9aa2b732 | ||
|
78af88e1f4 | ||
|
74c474a652 | ||
|
5e33ad29d4 | ||
|
27a99b19e8 | ||
|
ee0c897bba | ||
|
ceecf96208 | ||
|
47a3036ea6 | ||
|
d2dc31a74a | ||
|
60a9b938b4 | ||
|
dd517b9a55 | ||
|
b3329c362e | ||
|
b00f4a0cf3 | ||
|
a0ed88a99b | ||
|
25d2853db2 | ||
|
1930051bde | ||
|
6c34eafe02 | ||
|
bbff431e3a | ||
|
3660a321f0 | ||
|
e06f307c2d | ||
|
3b342be2f2 | ||
|
2dda356e3f | ||
|
972f6bc861 | ||
|
d2514445e1 | ||
|
daa46f14c4 | ||
|
1ada494bb2 | ||
|
4df26b2621 | ||
|
556f68ab15 | ||
|
fdb2689a14 | ||
|
598d3defd5 | ||
|
7f8044d913 | ||
|
66b39ccaed | ||
|
cae2a26ee3 | ||
|
b79ba3db8a | ||
|
59a77923b3 |
20
.travis.yml
@@ -1,7 +1,11 @@
|
||||
language: ruby
|
||||
cache: bundler
|
||||
cache:
|
||||
bundler: true
|
||||
yarn: true
|
||||
directories:
|
||||
- node_modules
|
||||
dist: trusty
|
||||
sudo: required
|
||||
sudo: false
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
@@ -14,6 +18,13 @@ env:
|
||||
- CXX=g++-4.8
|
||||
addons:
|
||||
postgresql: 9.4
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
- trusty-media
|
||||
packages:
|
||||
- g++-4.8
|
||||
- ffmpeg
|
||||
|
||||
rvm:
|
||||
- 2.3.4
|
||||
@@ -24,11 +35,6 @@ services:
|
||||
|
||||
bundler_args: --without development production --retry=3 --jobs=3
|
||||
|
||||
before_install:
|
||||
- sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
|
||||
- sudo add-apt-repository -y ppa:mc3man/trusty-media
|
||||
- sudo apt-get -qq update
|
||||
- sudo apt-get -qq install g++-4.8 ffmpeg
|
||||
install:
|
||||
- nvm install
|
||||
- npm install -g yarn
|
||||
|
@@ -17,6 +17,7 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
|
||||
postgresql-dev \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
python \
|
||||
build-base" \
|
||||
&& apk -U upgrade && apk add \
|
||||
$BUILD_DEPS \
|
||||
@@ -28,11 +29,13 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
|
||||
ffmpeg \
|
||||
file \
|
||||
imagemagick@edge \
|
||||
ca-certificates \
|
||||
&& npm install -g npm@3 && npm install -g yarn \
|
||||
&& bundle install --deployment --without test development \
|
||||
&& yarn --ignore-optional \
|
||||
&& yarn cache clean \
|
||||
&& npm -g cache clean \
|
||||
&& update-ca-certificates \
|
||||
&& apk del $BUILD_DEPS \
|
||||
&& rm -rf /tmp/* /var/cache/apk/*
|
||||
|
||||
|
2
Gemfile
@@ -35,6 +35,7 @@ gem 'http_accept_language'
|
||||
gem 'httplog'
|
||||
gem 'kaminari'
|
||||
gem 'link_header'
|
||||
gem 'local_time'
|
||||
gem 'nokogiri'
|
||||
gem 'oj'
|
||||
gem 'ostatus2', '~> 1.1'
|
||||
@@ -48,6 +49,7 @@ gem 'rails-settings-cached'
|
||||
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'rqrcode'
|
||||
gem 'ruby-oembed', require: 'oembed'
|
||||
gem 'sanitize'
|
||||
gem 'sidekiq'
|
||||
gem 'sidekiq-unique-jobs'
|
||||
gem 'simple-navigation'
|
||||
|
22
Gemfile.lock
@@ -111,11 +111,19 @@ GEM
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
coderay (1.1.1)
|
||||
coffee-rails (4.2.1)
|
||||
coffee-script (>= 2.2.0)
|
||||
railties (>= 4.0.0, < 5.2.x)
|
||||
coffee-script (2.4.1)
|
||||
coffee-script-source
|
||||
execjs
|
||||
coffee-script-source (1.12.2)
|
||||
colorize (0.8.1)
|
||||
concurrent-ruby (1.0.5)
|
||||
connection_pool (2.2.1)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.2)
|
||||
debug_inspector (0.0.2)
|
||||
devise (4.2.1)
|
||||
bcrypt (~> 3.0)
|
||||
@@ -157,7 +165,7 @@ GEM
|
||||
ruby-progressbar (~> 1.4)
|
||||
globalid (0.3.7)
|
||||
activesupport (>= 4.1.0)
|
||||
goldfinger (1.1.2)
|
||||
goldfinger (1.2.0)
|
||||
addressable (~> 2.4)
|
||||
http (~> 2.0)
|
||||
nokogiri (~> 1.6)
|
||||
@@ -224,6 +232,8 @@ GEM
|
||||
letter_opener (~> 1.0)
|
||||
railties (>= 3.2)
|
||||
link_header (0.0.8)
|
||||
local_time (1.0.3)
|
||||
coffee-rails
|
||||
lograge (0.4.1)
|
||||
actionpack (>= 4, < 5.1)
|
||||
activesupport (>= 4, < 5.1)
|
||||
@@ -249,6 +259,8 @@ GEM
|
||||
nio4r (2.0.0)
|
||||
nokogiri (1.7.1)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
nokogumbo (1.4.10)
|
||||
nokogiri
|
||||
oj (2.18.5)
|
||||
openssl (2.0.3)
|
||||
orm_adapter (0.5.0)
|
||||
@@ -389,6 +401,10 @@ GEM
|
||||
ruby-oembed (0.12.0)
|
||||
ruby-progressbar (1.8.1)
|
||||
safe_yaml (1.0.4)
|
||||
sanitize (4.4.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.4.4)
|
||||
nokogumbo (~> 1.4.1)
|
||||
sass (3.4.23)
|
||||
sass-rails (5.0.6)
|
||||
railties (>= 4.0.0, < 6)
|
||||
@@ -443,7 +459,7 @@ GEM
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.3)
|
||||
unf_ext (0.0.7.4)
|
||||
unicode-display_width (1.1.3)
|
||||
uniform_notifier (1.10.0)
|
||||
warden (1.2.7)
|
||||
@@ -500,6 +516,7 @@ DEPENDENCIES
|
||||
letter_opener
|
||||
letter_opener_web
|
||||
link_header
|
||||
local_time
|
||||
lograge
|
||||
microformats2
|
||||
nokogiri
|
||||
@@ -530,6 +547,7 @@ DEPENDENCIES
|
||||
rspec-sidekiq
|
||||
rubocop
|
||||
ruby-oembed
|
||||
sanitize
|
||||
sass-rails (~> 5.0)
|
||||
sidekiq
|
||||
sidekiq-unique-jobs
|
||||
|
21
Vagrantfile
vendored
@@ -35,22 +35,12 @@ sudo apt-get install \
|
||||
libreadline-dev \
|
||||
-y
|
||||
|
||||
# Install rbenv
|
||||
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
|
||||
cd ~/.rbenv && src/configure && make -C src
|
||||
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
|
||||
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
|
||||
|
||||
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
||||
|
||||
export PATH="$HOME/.rbenv/bin:$PATH"
|
||||
eval "$(rbenv init -)"
|
||||
|
||||
# Install rvm
|
||||
cd /vagrant
|
||||
|
||||
echo "Compiling Ruby $(cat .ruby-version): warning, this takes a while!!!"
|
||||
rbenv install $(cat .ruby-version)
|
||||
rbenv global $(cat .ruby-version)
|
||||
read RUBY_VERSION < .ruby-version
|
||||
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
|
||||
curl -sSL https://get.rvm.io | bash -s stable --ruby=$RUBY_VERSION
|
||||
source /home/vagrant/.rvm/scripts/rvm
|
||||
|
||||
# Configure database
|
||||
sudo -u postgres createuser -U postgres vagrant -s
|
||||
@@ -62,6 +52,7 @@ bundle install
|
||||
yarn install
|
||||
|
||||
# Build Mastodon
|
||||
export $(cat ".env.vagrant" | xargs)
|
||||
bundle exec rails db:setup
|
||||
bundle exec rails assets:precompile
|
||||
|
||||
|
0
app/assets/fonts/roboto-mono/robotomono-regular-webfont.eot
Executable file → Normal file
0
app/assets/fonts/roboto-mono/robotomono-regular-webfont.svg
Executable file → Normal file
Before Width: | Height: | Size: 339 KiB After Width: | Height: | Size: 339 KiB |
0
app/assets/fonts/roboto-mono/robotomono-regular-webfont.ttf
Executable file → Normal file
0
app/assets/fonts/roboto-mono/robotomono-regular-webfont.woff
Executable file → Normal file
0
app/assets/fonts/roboto-mono/robotomono-regular-webfont.woff2
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-bold-webfont.eot
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-bold-webfont.svg
Executable file → Normal file
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
0
app/assets/fonts/roboto/roboto-bold-webfont.ttf
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-bold-webfont.woff
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-bold-webfont.woff2
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-italic-webfont.eot
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-italic-webfont.svg
Executable file → Normal file
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
0
app/assets/fonts/roboto/roboto-italic-webfont.ttf
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-italic-webfont.woff
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-italic-webfont.woff2
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-medium-webfont.eot
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-medium-webfont.svg
Executable file → Normal file
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
0
app/assets/fonts/roboto/roboto-medium-webfont.ttf
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-medium-webfont.woff
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-medium-webfont.woff2
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-regular-webfont.eot
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-regular-webfont.svg
Executable file → Normal file
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
0
app/assets/fonts/roboto/roboto-regular-webfont.ttf
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-regular-webfont.woff
Executable file → Normal file
0
app/assets/fonts/roboto/roboto-regular-webfont.woff2
Executable file → Normal file
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 258 KiB |
@@ -10,6 +10,6 @@
|
||||
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
||||
// about supported directives.
|
||||
//
|
||||
//= require jquery
|
||||
//= require jquery2
|
||||
//= require jquery_ujs
|
||||
//= require components
|
||||
|
@@ -1,7 +1,8 @@
|
||||
//= require jquery
|
||||
//= require jquery2
|
||||
//= require jquery_ujs
|
||||
//= require extras
|
||||
//= require best_in_place
|
||||
//= require local_time
|
||||
|
||||
$(function () {
|
||||
$(".best_in_place").best_in_place();
|
||||
|
@@ -13,7 +13,7 @@ export function fetchStatusCard(id) {
|
||||
dispatch(fetchStatusCardRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/card`).then(response => {
|
||||
if (!response.data.url || !response.data.title || !response.data.description) {
|
||||
if (!response.data.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -85,6 +85,10 @@ export function submitCompose() {
|
||||
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
||||
visibility: getState().getIn(['compose', 'privacy'])
|
||||
}, {
|
||||
headers: {
|
||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey'])
|
||||
}
|
||||
}).then(function (response) {
|
||||
dispatch(submitComposeSuccess({ ...response.data }));
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import Avatar from './avatar';
|
||||
import DisplayName from './display_name';
|
||||
import Permalink from './permalink';
|
||||
@@ -10,39 +10,30 @@ const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }
|
||||
});
|
||||
|
||||
const buttonsStyle = {
|
||||
padding: '10px',
|
||||
height: '18px'
|
||||
};
|
||||
class Account extends React.PureComponent {
|
||||
|
||||
const Account = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
me: React.PropTypes.number.isRequired,
|
||||
onFollow: React.PropTypes.func.isRequired,
|
||||
onBlock: React.PropTypes.func.isRequired,
|
||||
onMute: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleFollow = this.handleFollow.bind(this);
|
||||
this.handleBlock = this.handleBlock.bind(this);
|
||||
this.handleMute = this.handleMute.bind(this);
|
||||
}
|
||||
|
||||
handleFollow () {
|
||||
this.props.onFollow(this.props.account);
|
||||
},
|
||||
}
|
||||
|
||||
handleBlock () {
|
||||
this.props.onBlock(this.props.account);
|
||||
},
|
||||
}
|
||||
|
||||
handleMute () {
|
||||
this.props.onMute(this.props.account);
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, me, intl } = this.props;
|
||||
@@ -72,13 +63,13 @@ const Account = React.createClass({
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div className='account__wrapper'>
|
||||
<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')} staticSrc={account.get('avatar_static')} size={36} /></div>
|
||||
<div className='account__avatar-wrapper'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
<div style={buttonsStyle}>
|
||||
<div className='account__relationship'>
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,6 +77,15 @@ const Account = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Account.propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
me: PropTypes.number.isRequired,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
export default injectIntl(Account);
|
||||
|
@@ -1,14 +1,8 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
|
||||
|
||||
const AttachmentList = React.createClass({
|
||||
propTypes: {
|
||||
media: ImmutablePropTypes.list.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class AttachmentList extends React.PureComponent {
|
||||
|
||||
render () {
|
||||
const { media } = this.props;
|
||||
@@ -29,6 +23,10 @@ const AttachmentList = React.createClass({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
AttachmentList.propTypes = {
|
||||
media: ImmutablePropTypes.list.isRequired
|
||||
};
|
||||
|
||||
export default AttachmentList;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isRtl } from '../rtl';
|
||||
|
||||
const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||
@@ -27,30 +28,23 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||
}
|
||||
};
|
||||
|
||||
const AutosuggestTextarea = React.createClass({
|
||||
class AutosuggestTextarea extends React.Component {
|
||||
|
||||
propTypes: {
|
||||
value: React.PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
disabled: React.PropTypes.bool,
|
||||
placeholder: React.PropTypes.string,
|
||||
onSuggestionSelected: React.PropTypes.func.isRequired,
|
||||
onSuggestionsClearRequested: React.PropTypes.func.isRequired,
|
||||
onSuggestionsFetchRequested: React.PropTypes.func.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
onKeyUp: React.PropTypes.func,
|
||||
onKeyDown: React.PropTypes.func,
|
||||
onPaste: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState () {
|
||||
return {
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
suggestionsHidden: false,
|
||||
selectedSuggestion: 0,
|
||||
lastToken: null,
|
||||
tokenStart: 0
|
||||
};
|
||||
},
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.onBlur = this.onBlur.bind(this);
|
||||
this.onSuggestionClick = this.onSuggestionClick.bind(this);
|
||||
this.setTextarea = this.setTextarea.bind(this);
|
||||
this.onPaste = this.onPaste.bind(this);
|
||||
}
|
||||
|
||||
onChange (e) {
|
||||
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
|
||||
@@ -68,7 +62,7 @@ const AutosuggestTextarea = React.createClass({
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
|
||||
this.props.onChange(e);
|
||||
},
|
||||
}
|
||||
|
||||
onKeyDown (e) {
|
||||
const { suggestions, disabled } = this.props;
|
||||
@@ -118,7 +112,7 @@ const AutosuggestTextarea = React.createClass({
|
||||
}
|
||||
|
||||
this.props.onKeyDown(e);
|
||||
},
|
||||
}
|
||||
|
||||
onBlur () {
|
||||
// If we hide the suggestions immediately, then this will prevent the
|
||||
@@ -128,36 +122,35 @@ const AutosuggestTextarea = React.createClass({
|
||||
setTimeout(() => {
|
||||
this.setState({ suggestionsHidden: true });
|
||||
}, 100);
|
||||
},
|
||||
}
|
||||
|
||||
onSuggestionClick (suggestion, e) {
|
||||
e.preventDefault();
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
|
||||
this.textarea.focus();
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
|
||||
this.setState({ suggestionsHidden: false });
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
setTextarea (c) {
|
||||
this.textarea = c;
|
||||
},
|
||||
}
|
||||
|
||||
onPaste (e) {
|
||||
if (e.clipboardData && e.clipboardData.files.length === 1) {
|
||||
this.props.onPaste(e.clipboardData.files)
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp } = this.props;
|
||||
const { suggestionsHidden, selectedSuggestion } = this.state;
|
||||
const className = 'autosuggest-textarea__textarea';
|
||||
const style = { direction: 'ltr' };
|
||||
const style = { direction: 'ltr' };
|
||||
|
||||
if (isRtl(value)) {
|
||||
style.direction = 'rtl';
|
||||
@@ -167,7 +160,7 @@ const AutosuggestTextarea = React.createClass({
|
||||
<div className='autosuggest-textarea'>
|
||||
<textarea
|
||||
ref={this.setTextarea}
|
||||
className={className}
|
||||
className='autosuggest-textarea__textarea'
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={true}
|
||||
@@ -196,6 +189,20 @@ const AutosuggestTextarea = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
AutosuggestTextarea.propTypes = {
|
||||
value: PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
disabled: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
||||
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onKeyUp: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AutosuggestTextarea;
|
||||
|
@@ -1,36 +1,23 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Avatar = React.createClass({
|
||||
class Avatar extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
src: React.PropTypes.string.isRequired,
|
||||
staticSrc: React.PropTypes.string,
|
||||
size: React.PropTypes.number.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
animate: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps () {
|
||||
return {
|
||||
animate: false
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState () {
|
||||
return {
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
hovering: false
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||
}
|
||||
|
||||
handleMouseEnter () {
|
||||
this.setState({ hovering: true });
|
||||
},
|
||||
}
|
||||
|
||||
handleMouseLeave () {
|
||||
this.setState({ hovering: false });
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { src, size, staticSrc, animate } = this.props;
|
||||
@@ -51,7 +38,7 @@ const Avatar = React.createClass({
|
||||
|
||||
return (
|
||||
<div
|
||||
className='avatar'
|
||||
className='account__avatar'
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
style={style}
|
||||
@@ -59,6 +46,18 @@ const Avatar = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Avatar.propTypes = {
|
||||
src: PropTypes.string.isRequired,
|
||||
staticSrc: PropTypes.string,
|
||||
size: PropTypes.number.isRequired,
|
||||
style: PropTypes.object,
|
||||
animate: PropTypes.bool
|
||||
};
|
||||
|
||||
Avatar.defaultProps = {
|
||||
animate: false
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
|
@@ -1,53 +1,25 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Button = React.createClass({
|
||||
class Button extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
text: React.PropTypes.node,
|
||||
onClick: React.PropTypes.func,
|
||||
disabled: React.PropTypes.bool,
|
||||
block: React.PropTypes.bool,
|
||||
secondary: React.PropTypes.bool,
|
||||
size: React.PropTypes.number,
|
||||
style: React.PropTypes.object,
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
|
||||
getDefaultProps () {
|
||||
return {
|
||||
size: 36
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick (e) {
|
||||
if (!this.props.disabled) {
|
||||
this.props.onClick();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const style = {
|
||||
fontFamily: 'inherit',
|
||||
display: this.props.block ? 'block' : 'inline-block',
|
||||
width: this.props.block ? '100%' : 'auto',
|
||||
position: 'relative',
|
||||
boxSizing: 'border-box',
|
||||
textAlign: 'center',
|
||||
border: '10px none',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
letterSpacing: '0',
|
||||
padding: `0 ${this.props.size / 2.25}px`,
|
||||
height: `${this.props.size}px`,
|
||||
cursor: 'pointer',
|
||||
lineHeight: `${this.props.size}px`,
|
||||
borderRadius: '4px',
|
||||
textDecoration: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden'
|
||||
lineHeight: `${this.props.size}px`
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -57,6 +29,21 @@ const Button = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Button.propTypes = {
|
||||
text: PropTypes.node,
|
||||
onClick: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
block: PropTypes.bool,
|
||||
secondary: PropTypes.bool,
|
||||
size: PropTypes.number,
|
||||
style: PropTypes.object,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
Button.defaultProps = {
|
||||
size: 36
|
||||
};
|
||||
|
||||
export default Button;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Motion, spring } from 'react-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Collapsable = ({ fullHeight, isVisible, children }) => (
|
||||
<Motion defaultStyle={{ opacity: !isVisible ? 0 : 100, height: isVisible ? fullHeight : 0 }} style={{ opacity: spring(!isVisible ? 0 : 100), height: spring(!isVisible ? 0 : fullHeight) }}>
|
||||
@@ -11,9 +12,9 @@ const Collapsable = ({ fullHeight, isVisible, children }) => (
|
||||
);
|
||||
|
||||
Collapsable.propTypes = {
|
||||
fullHeight: React.PropTypes.number.isRequired,
|
||||
isVisible: React.PropTypes.bool.isRequired,
|
||||
children: React.PropTypes.node.isRequired
|
||||
fullHeight: PropTypes.number.isRequired,
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
export default Collapsable;
|
||||
|
@@ -1,33 +1,31 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const iconStyle = {
|
||||
display: 'inline-block',
|
||||
marginRight: '5px'
|
||||
};
|
||||
class ColumnBackButton extends React.PureComponent {
|
||||
|
||||
const ColumnBackButton = React.createClass({
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick () {
|
||||
if (window.history && window.history.length === 1) this.context.router.push("/");
|
||||
else this.context.router.goBack();
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'>
|
||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||
<i className='fa fa-fw fa-chevron-left column-back-button__icon'/>
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
ColumnBackButton.contextTypes = {
|
||||
router: PropTypes.object
|
||||
};
|
||||
|
||||
export default ColumnBackButton;
|
||||
|
@@ -1,44 +1,31 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const outerStyle = {
|
||||
position: 'absolute',
|
||||
right: '0',
|
||||
top: '-48px',
|
||||
padding: '15px',
|
||||
fontSize: '16px',
|
||||
flex: '0 0 auto',
|
||||
cursor: 'pointer'
|
||||
};
|
||||
class ColumnBackButtonSlim extends React.PureComponent {
|
||||
|
||||
const iconStyle = {
|
||||
display: 'inline-block',
|
||||
marginRight: '5px'
|
||||
};
|
||||
|
||||
const ColumnBackButtonSlim = React.createClass({
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick () {
|
||||
this.context.router.push('/');
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div role='button' tabIndex='0' style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||
<div className='column-back-button--slim'>
|
||||
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
|
||||
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
ColumnBackButtonSlim.contextTypes = {
|
||||
router: PropTypes.object
|
||||
};
|
||||
|
||||
export default ColumnBackButtonSlim;
|
||||
|
@@ -1,33 +1,16 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const iconStyle = {
|
||||
fontSize: '16px',
|
||||
padding: '15px',
|
||||
position: 'absolute',
|
||||
right: '0',
|
||||
top: '-48px',
|
||||
cursor: 'pointer',
|
||||
zIndex: '3'
|
||||
};
|
||||
class ColumnCollapsable extends React.PureComponent {
|
||||
|
||||
const ColumnCollapsable = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
title: React.PropTypes.string,
|
||||
fullHeight: React.PropTypes.number.isRequired,
|
||||
children: React.PropTypes.node,
|
||||
onCollapse: React.PropTypes.func
|
||||
},
|
||||
|
||||
getInitialState () {
|
||||
return {
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
collapsed: true
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
this.handleToggleCollapsed = this.handleToggleCollapsed.bind(this);
|
||||
}
|
||||
|
||||
handleToggleCollapsed () {
|
||||
const currentState = this.state.collapsed;
|
||||
@@ -37,7 +20,7 @@ const ColumnCollapsable = React.createClass({
|
||||
if (!currentState && this.props.onCollapse) {
|
||||
this.props.onCollapse();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { icon, title, fullHeight, children } = this.props;
|
||||
@@ -45,8 +28,8 @@ const ColumnCollapsable = React.createClass({
|
||||
const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable';
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div role='button' tabIndex='0' title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}>
|
||||
<div className='column-collapsable'>
|
||||
<div role='button' tabIndex='0' title={`${title}`} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}>
|
||||
<i className={`fa fa-${icon}`} />
|
||||
</div>
|
||||
|
||||
@@ -60,6 +43,14 @@ const ColumnCollapsable = React.createClass({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ColumnCollapsable.propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
fullHeight: PropTypes.number.isRequired,
|
||||
children: PropTypes.node,
|
||||
onCollapse: PropTypes.func
|
||||
};
|
||||
|
||||
export default ColumnCollapsable;
|
||||
|
@@ -1,27 +1,24 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import emojify from '../emoji';
|
||||
|
||||
const DisplayName = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
account: ImmutablePropTypes.map.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class DisplayName extends React.PureComponent {
|
||||
|
||||
render () {
|
||||
const displayName = this.props.account.get('display_name').length === 0 ? this.props.account.get('username') : this.props.account.get('display_name');
|
||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
||||
|
||||
return (
|
||||
<span style={{ display: 'block', maxWidth: '100%', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }} className='display-name'>
|
||||
<strong style={{ fontWeight: '500' }} dangerouslySetInnerHTML={displayNameHTML} /> <span style={{ fontSize: '14px' }}>@{this.props.account.get('acct')}</span>
|
||||
<span className='display-name'>
|
||||
<strong className='display-name__html' dangerouslySetInnerHTML={displayNameHTML} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
DisplayName.propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired
|
||||
}
|
||||
|
||||
export default DisplayName;
|
||||
|
@@ -1,26 +1,20 @@
|
||||
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const DropdownMenu = React.createClass({
|
||||
class DropdownMenu extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
items: React.PropTypes.array.isRequired,
|
||||
size: React.PropTypes.number.isRequired,
|
||||
direction: React.PropTypes.string
|
||||
},
|
||||
|
||||
getDefaultProps () {
|
||||
return {
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
direction: 'left'
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
this.setRef = this.setRef.bind(this);
|
||||
this.renderItem = this.renderItem.bind(this);
|
||||
}
|
||||
|
||||
setRef (c) {
|
||||
this.dropdown = c;
|
||||
},
|
||||
}
|
||||
|
||||
handleClick (i, e) {
|
||||
const { action } = this.props.items[i];
|
||||
@@ -30,36 +24,36 @@ const DropdownMenu = React.createClass({
|
||||
action();
|
||||
this.dropdown.hide();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
renderItem (item, i) {
|
||||
if (item === null) {
|
||||
return <li key={i} className='dropdown__sep' />;
|
||||
return <li key={ 'sep' + i } className='dropdown__sep' />;
|
||||
}
|
||||
|
||||
const { text, action, href = '#' } = item;
|
||||
|
||||
return (
|
||||
<li key={i}>
|
||||
<a href={href} target='_blank' rel='noopener' onClick={this.handleClick.bind(this, i)}>
|
||||
<li className='dropdown__content-list-item' key={ text + i }>
|
||||
<a href={href} target='_blank' rel='noopener' onClick={this.handleClick.bind(this, i)} className='dropdown__content-list-link'>
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { icon, items, size, direction } = this.props;
|
||||
const { icon, items, size, direction, ariaLabel } = this.props;
|
||||
const directionClass = (direction === "left") ? "dropdown__left" : "dropdown__right";
|
||||
|
||||
return (
|
||||
<Dropdown ref={this.setRef}>
|
||||
<DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }}>
|
||||
<i className={`fa fa-fw fa-${icon}`} style={{ verticalAlign: 'middle' }} />
|
||||
<DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }} aria-label={ariaLabel}>
|
||||
<i className={ `fa fa-fw fa-${icon} dropdown__icon` } aria-hidden={true} />
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownContent className={directionClass} style={{ lineHeight: '18px', textAlign: 'left' }}>
|
||||
<ul>
|
||||
<DropdownContent className={directionClass}>
|
||||
<ul className='dropdown__content-list'>
|
||||
{items.map(this.renderItem)}
|
||||
</ul>
|
||||
</DropdownContent>
|
||||
@@ -67,6 +61,18 @@ const DropdownMenu = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
DropdownMenu.propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
direction: PropTypes.string,
|
||||
ariaLabel: PropTypes.string
|
||||
};
|
||||
|
||||
DropdownMenu.defaultProps = {
|
||||
ariaLabel: "Menu"
|
||||
};
|
||||
|
||||
export default DropdownMenu;
|
||||
|
@@ -1,33 +1,30 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ExtendedVideoPlayer = React.createClass({
|
||||
class ExtendedVideoPlayer extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
src: React.PropTypes.string.isRequired,
|
||||
time: React.PropTypes.number,
|
||||
controls: React.PropTypes.bool.isRequired,
|
||||
muted: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleLoadedData = this.handleLoadedData.bind(this);
|
||||
this.setRef = this.setRef.bind(this);
|
||||
}
|
||||
|
||||
handleLoadedData () {
|
||||
if (this.props.time) {
|
||||
this.video.currentTime = this.props.time;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.video.removeEventListener('loadeddata', this.handleLoadedData);
|
||||
},
|
||||
}
|
||||
|
||||
setRef (c) {
|
||||
this.video = c;
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
@@ -42,8 +39,15 @@ const ExtendedVideoPlayer = React.createClass({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
ExtendedVideoPlayer.propTypes = {
|
||||
src: PropTypes.string.isRequired,
|
||||
time: PropTypes.number,
|
||||
controls: PropTypes.bool.isRequired,
|
||||
muted: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default ExtendedVideoPlayer;
|
||||
|
@@ -1,33 +1,12 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const IconButton = React.createClass({
|
||||
class IconButton extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
title: React.PropTypes.string.isRequired,
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
onClick: React.PropTypes.func,
|
||||
size: React.PropTypes.number,
|
||||
active: React.PropTypes.bool,
|
||||
style: React.PropTypes.object,
|
||||
activeStyle: React.PropTypes.object,
|
||||
disabled: React.PropTypes.bool,
|
||||
inverted: React.PropTypes.bool,
|
||||
animate: React.PropTypes.bool,
|
||||
overlay: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps () {
|
||||
return {
|
||||
size: 18,
|
||||
active: false,
|
||||
disabled: false,
|
||||
animate: false,
|
||||
overlay: false
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick (e) {
|
||||
e.preventDefault();
|
||||
@@ -35,7 +14,7 @@ const IconButton = React.createClass({
|
||||
if (!this.props.disabled) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
let style = {
|
||||
@@ -68,6 +47,10 @@ const IconButton = React.createClass({
|
||||
classes.push('overlayed');
|
||||
}
|
||||
|
||||
if (this.props.className) {
|
||||
classes.push(this.props.className)
|
||||
}
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
|
||||
{({ rotate }) =>
|
||||
@@ -84,6 +67,29 @@ const IconButton = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
IconButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
size: PropTypes.number,
|
||||
active: PropTypes.bool,
|
||||
style: PropTypes.object,
|
||||
activeStyle: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
inverted: PropTypes.bool,
|
||||
animate: PropTypes.bool,
|
||||
overlay: PropTypes.bool
|
||||
};
|
||||
|
||||
IconButton.defaultProps = {
|
||||
size: 18,
|
||||
active: false,
|
||||
disabled: false,
|
||||
animate: false,
|
||||
overlay: false
|
||||
};
|
||||
|
||||
export default IconButton;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const LoadMore = ({ onClick }) => (
|
||||
<a href="#" className='load-more' role='button' onClick={onClick}>
|
||||
@@ -7,7 +8,7 @@ const LoadMore = ({ onClick }) => (
|
||||
);
|
||||
|
||||
LoadMore.propTypes = {
|
||||
onClick: React.PropTypes.func
|
||||
onClick: PropTypes.func
|
||||
};
|
||||
|
||||
export default LoadMore;
|
||||
|
@@ -1,14 +1,7 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const style = {
|
||||
textAlign: 'center',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
paddingTop: '120px'
|
||||
};
|
||||
|
||||
const LoadingIndicator = () => (
|
||||
<div className='loading-indicator' style={style}>
|
||||
<div className='loading-indicator'>
|
||||
<FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
|
||||
</div>
|
||||
);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from './icon_button';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { isIOS } from '../is_mobile';
|
||||
@@ -8,81 +8,11 @@ const messages = defineMessages({
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }
|
||||
});
|
||||
|
||||
const outerStyle = {
|
||||
marginTop: '8px',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
position: 'relative'
|
||||
};
|
||||
|
||||
const spoilerStyle = {
|
||||
textAlign: 'center',
|
||||
height: '100%',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column'
|
||||
};
|
||||
|
||||
const spoilerSpanStyle = {
|
||||
display: 'block',
|
||||
fontSize: '14px',
|
||||
};
|
||||
|
||||
const spoilerSubSpanStyle = {
|
||||
display: 'block',
|
||||
fontSize: '11px',
|
||||
fontWeight: '500'
|
||||
};
|
||||
|
||||
const spoilerButtonStyle = {
|
||||
position: 'absolute',
|
||||
top: '4px',
|
||||
left: '4px',
|
||||
zIndex: '100'
|
||||
};
|
||||
|
||||
const itemStyle = {
|
||||
boxSizing: 'border-box',
|
||||
position: 'relative',
|
||||
float: 'left',
|
||||
border: 'none',
|
||||
display: 'block'
|
||||
};
|
||||
|
||||
const thumbStyle = {
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
textDecoration: 'none',
|
||||
backgroundSize: 'cover',
|
||||
cursor: 'zoom-in'
|
||||
};
|
||||
|
||||
const gifvThumbStyle = {
|
||||
position: 'relative',
|
||||
zIndex: '1',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
cursor: 'zoom-in'
|
||||
};
|
||||
|
||||
const Item = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
attachment: ImmutablePropTypes.map.isRequired,
|
||||
index: React.PropTypes.number.isRequired,
|
||||
size: React.PropTypes.number.isRequired,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
autoPlayGif: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class Item extends React.PureComponent {
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick (e) {
|
||||
const { index, onClick } = this.props;
|
||||
@@ -93,7 +23,7 @@ const Item = React.createClass({
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { attachment, index, size } = this.props;
|
||||
@@ -152,24 +82,26 @@ const Item = React.createClass({
|
||||
if (attachment.get('type') === 'image') {
|
||||
thumbnail = (
|
||||
<a
|
||||
href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')}
|
||||
className='media-gallery__item-thumbnail'
|
||||
href={attachment.get('remote_url') || attachment.get('url')}
|
||||
onClick={this.handleClick}
|
||||
target='_blank'
|
||||
style={{ background: `url(${attachment.get('preview_url')}) no-repeat center`, ...thumbStyle }}
|
||||
style={{ backgroundImage: `url(${attachment.get('preview_url')})` }}
|
||||
/>
|
||||
);
|
||||
} else if (attachment.get('type') === 'gifv') {
|
||||
const autoPlay = !isIOS() && this.props.autoPlayGif;
|
||||
|
||||
thumbnail = (
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }} className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}>
|
||||
<div className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}>
|
||||
<video
|
||||
className='media-gallery__item-gifv-thumbnail'
|
||||
role='application'
|
||||
src={attachment.get('url')}
|
||||
onClick={this.handleClick}
|
||||
autoPlay={autoPlay}
|
||||
loop={true}
|
||||
muted={true}
|
||||
style={gifvThumbStyle}
|
||||
/>
|
||||
|
||||
<span className='media-gallery__gifv__label'>GIF</span>
|
||||
@@ -178,40 +110,40 @@ const Item = React.createClass({
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={attachment.get('id')} style={{ ...itemStyle, left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||
<div className='media-gallery__item' key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||
{thumbnail}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
const MediaGallery = React.createClass({
|
||||
Item.propTypes = {
|
||||
attachment: ImmutablePropTypes.map.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
autoPlayGif: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
getInitialState () {
|
||||
return {
|
||||
visible: !this.props.sensitive
|
||||
class MediaGallery extends React.PureComponent {
|
||||
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
visible: !props.sensitive
|
||||
};
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
sensitive: React.PropTypes.bool,
|
||||
media: ImmutablePropTypes.list.isRequired,
|
||||
height: React.PropTypes.number.isRequired,
|
||||
onOpenMedia: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
autoPlayGif: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
this.handleOpen = this.handleOpen.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleOpen (e) {
|
||||
this.setState({ visible: !this.state.visible });
|
||||
},
|
||||
}
|
||||
|
||||
handleClick (index) {
|
||||
this.props.onOpenMedia(this.props.media, index);
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media, intl, sensitive } = this.props;
|
||||
@@ -228,9 +160,9 @@ const MediaGallery = React.createClass({
|
||||
}
|
||||
|
||||
children = (
|
||||
<div role='button' tabIndex='0' style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
||||
<span style={spoilerSpanStyle}>{warning}</span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
<div role='button' tabIndex='0' className='media-spoiler' onClick={this.handleOpen}>
|
||||
<span className='media-spoiler__warning'>{warning}</span>
|
||||
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
@@ -239,8 +171,8 @@ const MediaGallery = React.createClass({
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ ...outerStyle, height: `${this.props.height}px` }}>
|
||||
<div style={{ ...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block' }}>
|
||||
<div className='media-gallery' style={{ height: `${this.props.height}px` }}>
|
||||
<div className='spoiler-button' style={{ display: !this.state.visible ? 'none' : 'block' }}>
|
||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
|
||||
</div>
|
||||
|
||||
@@ -249,6 +181,15 @@ const MediaGallery = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
MediaGallery.propTypes = {
|
||||
sensitive: PropTypes.bool,
|
||||
media: ImmutablePropTypes.list.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
onOpenMedia: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
autoPlayGif: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(MediaGallery);
|
||||
|
@@ -1,28 +1,36 @@
|
||||
const Permalink = React.createClass({
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object
|
||||
},
|
||||
class Permalink extends React.Component {
|
||||
|
||||
propTypes: {
|
||||
href: React.PropTypes.string.isRequired,
|
||||
to: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick (e) {
|
||||
if (e.button === 0) {
|
||||
e.preventDefault();
|
||||
this.context.router.push(this.props.to);
|
||||
}
|
||||
},
|
||||
|
||||
render () {
|
||||
const { href, children, ...other } = this.props;
|
||||
|
||||
return <a href={href} onClick={this.handleClick} {...other}>{children}</a>;
|
||||
}
|
||||
|
||||
});
|
||||
render () {
|
||||
const { href, children, className, ...other } = this.props;
|
||||
|
||||
return <a href={href} onClick={this.handleClick} {...other} className={'permalink ' + className}>{children}</a>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Permalink.contextTypes = {
|
||||
router: PropTypes.object
|
||||
};
|
||||
|
||||
Permalink.propTypes = {
|
||||
className: PropTypes.string,
|
||||
href: PropTypes.string.isRequired,
|
||||
to: PropTypes.string.isRequired,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export default Permalink;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { injectIntl, FormattedRelative } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const RelativeTimestamp = ({ intl, timestamp }) => {
|
||||
const date = new Date(timestamp);
|
||||
@@ -11,8 +12,8 @@ const RelativeTimestamp = ({ intl, timestamp }) => {
|
||||
};
|
||||
|
||||
RelativeTimestamp.propTypes = {
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
timestamp: React.PropTypes.string.isRequired
|
||||
intl: PropTypes.object.isRequired,
|
||||
timestamp: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(RelativeTimestamp);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import Avatar from './avatar';
|
||||
import RelativeTimestamp from './relative_timestamp';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import DisplayName from './display_name';
|
||||
import MediaGallery from './media_gallery';
|
||||
import VideoPlayer from './video_player';
|
||||
@@ -12,41 +12,25 @@ import { FormattedMessage } from 'react-intl';
|
||||
import emojify from '../emoji';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
|
||||
const Status = React.createClass({
|
||||
class Status extends React.PureComponent {
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
status: ImmutablePropTypes.map,
|
||||
wrapped: React.PropTypes.bool,
|
||||
onReply: React.PropTypes.func,
|
||||
onFavourite: React.PropTypes.func,
|
||||
onReblog: React.PropTypes.func,
|
||||
onDelete: React.PropTypes.func,
|
||||
onOpenMedia: React.PropTypes.func,
|
||||
onOpenVideo: React.PropTypes.func,
|
||||
onBlock: React.PropTypes.func,
|
||||
me: React.PropTypes.number,
|
||||
boostModal: React.PropTypes.bool,
|
||||
autoPlayGif: React.PropTypes.bool,
|
||||
muted: React.PropTypes.bool
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleAccountClick = this.handleAccountClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick () {
|
||||
const { status } = this.props;
|
||||
this.context.router.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
|
||||
},
|
||||
}
|
||||
|
||||
handleAccountClick (id, e) {
|
||||
if (e.button === 0) {
|
||||
e.preventDefault();
|
||||
this.context.router.push(`/accounts/${id}`);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
let media = '';
|
||||
@@ -66,10 +50,10 @@ const Status = React.createClass({
|
||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
||||
|
||||
return (
|
||||
<div style={{ cursor: 'default' }}>
|
||||
<div className='status__wrapper'>
|
||||
<div className='status__prepend'>
|
||||
<div style={{ position: 'absolute', 'left': '-26px'}}><i className='fa fa-fw fa-retweet' /></div>
|
||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} reblogged' values={{ name: <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
|
||||
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
|
||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
|
||||
</div>
|
||||
|
||||
<Status {...other} wrapped={true} status={status.get('reblog')} />
|
||||
@@ -89,13 +73,13 @@ const Status = React.createClass({
|
||||
|
||||
return (
|
||||
<div className={this.props.muted ? 'status muted' : 'status'}>
|
||||
<div style={{ fontSize: '15px' }}>
|
||||
<div style={{ float: 'right', fontSize: '14px' }}>
|
||||
<div className='status__info'>
|
||||
<div className='status__info-time'>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
</div>
|
||||
|
||||
<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' }}>
|
||||
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name'>
|
||||
<div className='status__avatar'>
|
||||
<Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
|
||||
</div>
|
||||
|
||||
@@ -112,6 +96,26 @@ const Status = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Status.contextTypes = {
|
||||
router: PropTypes.object
|
||||
};
|
||||
|
||||
Status.propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
wrapped: PropTypes.bool,
|
||||
onReply: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onOpenMedia: PropTypes.func,
|
||||
onOpenVideo: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
me: PropTypes.number,
|
||||
boostModal: PropTypes.bool,
|
||||
autoPlayGif: PropTypes.bool,
|
||||
muted: PropTypes.bool
|
||||
};
|
||||
|
||||
export default Status;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from './icon_button';
|
||||
import DropdownMenu from './dropdown_menu';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
@@ -11,73 +11,68 @@ const messages = defineMessages({
|
||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||
open: { id: 'status.open', defaultMessage: 'Expand this status' },
|
||||
report: { id: 'status.report', defaultMessage: 'Report @{name}' }
|
||||
});
|
||||
|
||||
const StatusActionBar = React.createClass({
|
||||
class StatusActionBar extends React.PureComponent {
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
onReply: React.PropTypes.func,
|
||||
onFavourite: React.PropTypes.func,
|
||||
onReblog: React.PropTypes.func,
|
||||
onDelete: React.PropTypes.func,
|
||||
onMention: React.PropTypes.func,
|
||||
onMute: React.PropTypes.func,
|
||||
onBlock: React.PropTypes.func,
|
||||
onReport: React.PropTypes.func,
|
||||
me: React.PropTypes.number.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleReplyClick = this.handleReplyClick.bind(this);
|
||||
this.handleFavouriteClick = this.handleFavouriteClick.bind(this);
|
||||
this.handleReblogClick = this.handleReblogClick.bind(this);
|
||||
this.handleDeleteClick = this.handleDeleteClick.bind(this);
|
||||
this.handleMentionClick = this.handleMentionClick.bind(this);
|
||||
this.handleMuteClick = this.handleMuteClick.bind(this);
|
||||
this.handleBlockClick = this.handleBlockClick.bind(this);
|
||||
this.handleOpen = this.handleOpen.bind(this);
|
||||
this.handleReport = this.handleReport.bind(this);
|
||||
}
|
||||
|
||||
handleReplyClick () {
|
||||
this.props.onReply(this.props.status, this.context.router);
|
||||
},
|
||||
}
|
||||
|
||||
handleFavouriteClick () {
|
||||
this.props.onFavourite(this.props.status);
|
||||
},
|
||||
}
|
||||
|
||||
handleReblogClick (e) {
|
||||
this.props.onReblog(this.props.status, e);
|
||||
},
|
||||
}
|
||||
|
||||
handleDeleteClick () {
|
||||
this.props.onDelete(this.props.status);
|
||||
},
|
||||
}
|
||||
|
||||
handleMentionClick () {
|
||||
this.props.onMention(this.props.status.get('account'), this.context.router);
|
||||
},
|
||||
}
|
||||
|
||||
handleMuteClick () {
|
||||
this.props.onMute(this.props.status.get('account'));
|
||||
},
|
||||
}
|
||||
|
||||
handleBlockClick () {
|
||||
this.props.onBlock(this.props.status.get('account'));
|
||||
},
|
||||
}
|
||||
|
||||
handleOpen () {
|
||||
this.context.router.push(`/statuses/${this.props.status.get('id')}`);
|
||||
},
|
||||
}
|
||||
|
||||
handleReport () {
|
||||
this.props.onReport(this.props.status);
|
||||
this.context.router.push('/report');
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { status, me, intl } = this.props;
|
||||
const reblog_disabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
|
||||
let menu = [];
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
||||
@@ -107,18 +102,36 @@ const StatusActionBar = React.createClass({
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title={reply_title} icon={reply_icon} onClick={this.handleReplyClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private' || status.get('visibility') === 'direct'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
|
||||
<div className='status__action-bar'>
|
||||
<div className='status__action-bar-button-wrapper'><IconButton title={reply_title} icon={reply_icon} onClick={this.handleReplyClick} /></div>
|
||||
<div className='status__action-bar-button-wrapper'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
|
||||
<div className='status__action-bar-button-wrapper'><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} className='star-icon' /></div>
|
||||
|
||||
<div style={{ width: '18px', height: '18px', float: 'left' }}>
|
||||
<DropdownMenu items={menu} icon='ellipsis-h' size={18} direction="right" />
|
||||
<div className='status__action-bar-dropdown'>
|
||||
<DropdownMenu items={menu} icon='ellipsis-h' size={18} direction="right" ariaLabel="More"/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
StatusActionBar.contextTypes = {
|
||||
router: PropTypes.object
|
||||
};
|
||||
|
||||
StatusActionBar.propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
onReply: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onMention: PropTypes.func,
|
||||
onMute: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
onReport: PropTypes.func,
|
||||
me: PropTypes.number.isRequired,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(StatusActionBar);
|
||||
|
@@ -1,29 +1,24 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import PropTypes from 'prop-types';
|
||||
import emojify from '../emoji';
|
||||
import { isRtl } from '../rtl';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Permalink from './permalink';
|
||||
|
||||
const StatusContent = React.createClass({
|
||||
class StatusContent extends React.PureComponent {
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
onClick: React.PropTypes.func
|
||||
},
|
||||
|
||||
getInitialState () {
|
||||
return {
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
hidden: true
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
this.onMentionClick = this.onMentionClick.bind(this);
|
||||
this.onHashtagClick = this.onHashtagClick.bind(this);
|
||||
this.handleMouseDown = this.handleMouseDown.bind(this)
|
||||
this.handleMouseUp = this.handleMouseUp.bind(this);
|
||||
this.handleSpoilerClick = this.handleSpoilerClick.bind(this);
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const node = ReactDOM.findDOMNode(this);
|
||||
@@ -47,14 +42,14 @@ const StatusContent = React.createClass({
|
||||
link.setAttribute('title', link.href);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onMentionClick (mention, e) {
|
||||
if (e.button === 0) {
|
||||
e.preventDefault();
|
||||
this.context.router.push(`/accounts/${mention.get('id')}`);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onHashtagClick (hashtag, e) {
|
||||
hashtag = hashtag.replace(/^#/, '').toLowerCase();
|
||||
@@ -63,11 +58,11 @@ const StatusContent = React.createClass({
|
||||
e.preventDefault();
|
||||
this.context.router.push(`/timelines/tag/${hashtag}`);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
this.startXY = [e.clientX, e.clientY];
|
||||
},
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
const [ startX, startY ] = this.startXY;
|
||||
@@ -82,12 +77,12 @@ const StatusContent = React.createClass({
|
||||
}
|
||||
|
||||
this.startXY = null;
|
||||
},
|
||||
}
|
||||
|
||||
handleSpoilerClick (e) {
|
||||
e.preventDefault();
|
||||
this.setState({ hidden: !this.state.hidden });
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { status } = this.props;
|
||||
@@ -117,7 +112,7 @@ const StatusContent = React.createClass({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||
<div className='status__content' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
|
||||
<span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a>
|
||||
</p>
|
||||
@@ -131,7 +126,7 @@ const StatusContent = React.createClass({
|
||||
return (
|
||||
<div
|
||||
className='status__content'
|
||||
style={{ cursor: 'pointer', ...directionStyle }}
|
||||
style={{ ...directionStyle }}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onMouseUp={this.handleMouseUp}
|
||||
dangerouslySetInnerHTML={content}
|
||||
@@ -140,14 +135,23 @@ const StatusContent = React.createClass({
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className='status__content'
|
||||
className='status__content status__content--no-action'
|
||||
style={{ ...directionStyle }}
|
||||
dangerouslySetInnerHTML={content}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
StatusContent.contextTypes = {
|
||||
router: PropTypes.object
|
||||
};
|
||||
|
||||
StatusContent.propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
onClick: PropTypes.func
|
||||
};
|
||||
|
||||
export default StatusContent;
|
||||
|
@@ -1,32 +1,18 @@
|
||||
import Status from './status';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import { ScrollContainer } from 'react-router-scroll';
|
||||
import PropTypes from 'prop-types';
|
||||
import StatusContainer from '../containers/status_container';
|
||||
import LoadMore from './load_more';
|
||||
|
||||
const StatusList = React.createClass({
|
||||
class StatusList extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
onScrollToBottom: React.PropTypes.func,
|
||||
onScrollToTop: React.PropTypes.func,
|
||||
onScroll: React.PropTypes.func,
|
||||
trackScroll: React.PropTypes.bool,
|
||||
isLoading: React.PropTypes.bool,
|
||||
isUnread: React.PropTypes.bool,
|
||||
hasMore: React.PropTypes.bool,
|
||||
prepend: React.PropTypes.node,
|
||||
emptyMessage: React.PropTypes.node
|
||||
},
|
||||
|
||||
getDefaultProps () {
|
||||
return {
|
||||
trackScroll: true
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
this.setRef = this.setRef.bind(this);
|
||||
this.handleLoadMore = this.handleLoadMore.bind(this);
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
@@ -40,41 +26,41 @@ const StatusList = React.createClass({
|
||||
} else if (this.props.onScroll) {
|
||||
this.props.onScroll();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.attachScrollListener();
|
||||
},
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.node.scrollTop > 0 && (prevProps.statusIds.size < this.props.statusIds.size && prevProps.statusIds.first() !== this.props.statusIds.first() && !!this._oldScrollPosition)) {
|
||||
this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.detachScrollListener();
|
||||
},
|
||||
}
|
||||
|
||||
attachScrollListener () {
|
||||
this.node.addEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
}
|
||||
|
||||
detachScrollListener () {
|
||||
this.node.removeEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
}
|
||||
|
||||
setRef (c) {
|
||||
this.node = c;
|
||||
},
|
||||
}
|
||||
|
||||
handleLoadMore (e) {
|
||||
e.preventDefault();
|
||||
this.props.onScrollToBottom();
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { statusIds, onScrollToBottom, trackScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props;
|
||||
const { statusIds, onScrollToBottom, scrollKey, shouldUpdateScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props;
|
||||
|
||||
let loadMore = '';
|
||||
let scrollableArea = '';
|
||||
@@ -93,7 +79,7 @@ const StatusList = React.createClass({
|
||||
<div className='scrollable' ref={this.setRef}>
|
||||
{unread}
|
||||
|
||||
<div>
|
||||
<div className='status-list'>
|
||||
{prepend}
|
||||
|
||||
{statusIds.map((statusId) => {
|
||||
@@ -112,17 +98,31 @@ const StatusList = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
if (trackScroll) {
|
||||
return (
|
||||
<ScrollContainer scrollKey='status-list'>
|
||||
{scrollableArea}
|
||||
</ScrollContainer>
|
||||
);
|
||||
} else {
|
||||
return scrollableArea;
|
||||
}
|
||||
return (
|
||||
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
|
||||
{scrollableArea}
|
||||
</ScrollContainer>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
StatusList.propTypes = {
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
onScrollToBottom: PropTypes.func,
|
||||
onScrollToTop: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
shouldUpdateScroll: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
isUnread: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
prepend: PropTypes.node,
|
||||
emptyMessage: PropTypes.node
|
||||
};
|
||||
|
||||
StatusList.defaultProps = {
|
||||
trackScroll: true
|
||||
};
|
||||
|
||||
export default StatusList;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from './icon_button';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { isIOS } from '../is_mobile';
|
||||
@@ -11,100 +11,31 @@ const messages = defineMessages({
|
||||
expand_video: { id: 'video_player.video_error', defaultMessage: 'Video could not be played' }
|
||||
});
|
||||
|
||||
const videoStyle = {
|
||||
position: 'relative',
|
||||
zIndex: '1',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)'
|
||||
};
|
||||
class VideoPlayer extends React.PureComponent {
|
||||
|
||||
const muteStyle = {
|
||||
position: 'absolute',
|
||||
top: '4px',
|
||||
right: '4px',
|
||||
color: 'white',
|
||||
textShadow: "0px 1px 1px black, 1px 0px 1px black",
|
||||
opacity: '0.8',
|
||||
zIndex: '5'
|
||||
};
|
||||
|
||||
const coverStyle = {
|
||||
marginTop: '8px',
|
||||
textAlign: 'center',
|
||||
height: '100%',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
position: 'relative'
|
||||
};
|
||||
|
||||
const spoilerSpanStyle = {
|
||||
display: 'block',
|
||||
fontSize: '14px'
|
||||
};
|
||||
|
||||
const spoilerSubSpanStyle = {
|
||||
display: 'block',
|
||||
fontSize: '11px',
|
||||
fontWeight: '500'
|
||||
};
|
||||
|
||||
const spoilerButtonStyle = {
|
||||
position: 'absolute',
|
||||
top: '4px',
|
||||
left: '4px',
|
||||
color: 'white',
|
||||
textShadow: "0px 1px 1px black, 1px 0px 1px black",
|
||||
zIndex: '100'
|
||||
};
|
||||
|
||||
const expandButtonStyle = {
|
||||
position: 'absolute',
|
||||
bottom: '4px',
|
||||
right: '4px',
|
||||
color: 'white',
|
||||
textShadow: "0px 1px 1px black, 1px 0px 1px black",
|
||||
zIndex: '100'
|
||||
};
|
||||
|
||||
const VideoPlayer = React.createClass({
|
||||
propTypes: {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
sensitive: React.PropTypes.bool,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
autoplay: React.PropTypes.bool,
|
||||
onOpenVideo: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
getDefaultProps () {
|
||||
return {
|
||||
width: 239,
|
||||
height: 110
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState () {
|
||||
return {
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
visible: !this.props.sensitive,
|
||||
preview: true,
|
||||
muted: true,
|
||||
hasAudio: true,
|
||||
videoError: false
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleVideoClick = this.handleVideoClick.bind(this);
|
||||
this.handleOpen = this.handleOpen.bind(this);
|
||||
this.handleVisibility = this.handleVisibility.bind(this);
|
||||
this.handleExpand = this.handleExpand.bind(this);
|
||||
this.setRef = this.setRef.bind(this);
|
||||
this.handleLoadedData = this.handleLoadedData.bind(this);
|
||||
this.handleVideoError = this.handleVideoError.bind(this);
|
||||
}
|
||||
|
||||
handleClick () {
|
||||
this.setState({ muted: !this.state.muted });
|
||||
},
|
||||
}
|
||||
|
||||
handleVideoClick (e) {
|
||||
e.stopPropagation();
|
||||
@@ -116,37 +47,37 @@ const VideoPlayer = React.createClass({
|
||||
} else {
|
||||
node.pause();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleOpen () {
|
||||
this.setState({ preview: !this.state.preview });
|
||||
},
|
||||
}
|
||||
|
||||
handleVisibility () {
|
||||
this.setState({
|
||||
visible: !this.state.visible,
|
||||
preview: true
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
handleExpand () {
|
||||
this.video.pause();
|
||||
this.props.onOpenVideo(this.props.media, this.video.currentTime);
|
||||
},
|
||||
}
|
||||
|
||||
setRef (c) {
|
||||
this.video = c;
|
||||
},
|
||||
}
|
||||
|
||||
handleLoadedData () {
|
||||
if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) {
|
||||
this.setState({ hasAudio: false });
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleVideoError () {
|
||||
this.setState({ videoError: true });
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (!this.video) {
|
||||
@@ -155,7 +86,7 @@ const VideoPlayer = React.createClass({
|
||||
|
||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||
this.video.addEventListener('error', this.handleVideoError);
|
||||
},
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
if (!this.video) {
|
||||
@@ -164,7 +95,7 @@ const VideoPlayer = React.createClass({
|
||||
|
||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||
this.video.addEventListener('error', this.handleVideoError);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (!this.video) {
|
||||
@@ -173,19 +104,19 @@ const VideoPlayer = React.createClass({
|
||||
|
||||
this.video.removeEventListener('loadeddata', this.handleLoadedData);
|
||||
this.video.removeEventListener('error', this.handleVideoError);
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media, intl, width, height, sensitive, autoplay } = this.props;
|
||||
|
||||
let spoilerButton = (
|
||||
<div style={{...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block'}} >
|
||||
<div className='status__video-player-spoiler' style={{ display: !this.state.visible ? 'none' : 'block' }} >
|
||||
<IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
|
||||
</div>
|
||||
);
|
||||
|
||||
let expandButton = (
|
||||
<div style={expandButtonStyle} >
|
||||
<div className='status__video-player-expand'>
|
||||
<IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
|
||||
</div>
|
||||
);
|
||||
@@ -194,7 +125,7 @@ const VideoPlayer = React.createClass({
|
||||
|
||||
if (this.state.hasAudio) {
|
||||
muteButton = (
|
||||
<div style={muteStyle}>
|
||||
<div className='status__video-player-mute'>
|
||||
<IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
|
||||
</div>
|
||||
);
|
||||
@@ -203,18 +134,18 @@ const VideoPlayer = React.createClass({
|
||||
if (!this.state.visible) {
|
||||
if (sensitive) {
|
||||
return (
|
||||
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
{spoilerButton}
|
||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
<span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
||||
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
{spoilerButton}
|
||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
<span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
||||
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -222,31 +153,46 @@ const VideoPlayer = React.createClass({
|
||||
|
||||
if (this.state.preview && !autoplay) {
|
||||
return (
|
||||
<div role='button' tabIndex='0' style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
|
||||
<div role='button' tabIndex='0' className='media-spoiler-video' style={{ width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center` }} onClick={this.handleOpen}>
|
||||
{spoilerButton}
|
||||
<div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
|
||||
<div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.videoError) {
|
||||
return (
|
||||
<div style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='video-error-cover' >
|
||||
<span style={spoilerSpanStyle}><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
|
||||
<div style={{ width: `${width}px`, height: `${height}px` }} className='video-error-cover' >
|
||||
<span className='media-spoiler__warning'><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
|
||||
<div className='status__video-player' style={{ width: `${width}px`, height: `${height}px` }}>
|
||||
{spoilerButton}
|
||||
{muteButton}
|
||||
{expandButton}
|
||||
<video role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
||||
<video className='status__video-player-video' role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} onClick={this.handleVideoClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
VideoPlayer.propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
sensitive: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
autoplay: PropTypes.bool,
|
||||
onOpenVideo: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
VideoPlayer.defaultProps = {
|
||||
width: 239,
|
||||
height: 110
|
||||
};
|
||||
|
||||
export default injectIntl(VideoPlayer);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Provider } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import configureStore from '../store/configureStore';
|
||||
import {
|
||||
refreshTimelineSuccess,
|
||||
@@ -41,10 +42,12 @@ import Blocks from '../features/blocks';
|
||||
import Mutes from '../features/mutes';
|
||||
import Report from '../features/report';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import ar from 'react-intl/locale-data/ar';
|
||||
import en from 'react-intl/locale-data/en';
|
||||
import de from 'react-intl/locale-data/de';
|
||||
import eo from 'react-intl/locale-data/eo';
|
||||
import es from 'react-intl/locale-data/es';
|
||||
import fa from 'react-intl/locale-data/fa';
|
||||
import fi from 'react-intl/locale-data/fi';
|
||||
import fr from 'react-intl/locale-data/fr';
|
||||
import hu from 'react-intl/locale-data/hu';
|
||||
@@ -57,7 +60,9 @@ import ru from 'react-intl/locale-data/ru';
|
||||
import uk from 'react-intl/locale-data/uk';
|
||||
import zh from 'react-intl/locale-data/zh';
|
||||
import bg from 'react-intl/locale-data/bg';
|
||||
import id from 'react-intl/locale-data/id';
|
||||
import { localeData as zh_hk } from '../locales/zh-hk';
|
||||
import { localeData as zh_cn } from '../locales/zh-cn';
|
||||
import pt_br from '../locales/pt-br';
|
||||
import getMessagesForLocale from '../locales';
|
||||
import { hydrateStore } from '../actions/store';
|
||||
@@ -73,9 +78,11 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
|
||||
|
||||
addLocaleData([
|
||||
...en,
|
||||
...ar,
|
||||
...de,
|
||||
...eo,
|
||||
...es,
|
||||
...fa,
|
||||
...fi,
|
||||
...fr,
|
||||
...hu,
|
||||
@@ -89,14 +96,131 @@ addLocaleData([
|
||||
...uk,
|
||||
...zh,
|
||||
...zh_hk,
|
||||
...zh_cn,
|
||||
...bg,
|
||||
...id,
|
||||
]);
|
||||
|
||||
const Mastodon = React.createClass({
|
||||
const getTopWhenReplacing = (previous, { location }) => location && location.action === 'REPLACE' && [0, 0];
|
||||
|
||||
propTypes: {
|
||||
locale: React.PropTypes.string.isRequired
|
||||
},
|
||||
const hiddenColumnContainerStyle = {
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
visibility: 'hidden'
|
||||
};
|
||||
|
||||
class Container extends React.PureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
renderedPersistents: [],
|
||||
unrenderedPersistents: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.unlistenHistory = null;
|
||||
|
||||
this.setState(() => {
|
||||
return {
|
||||
mountImpersistent: false,
|
||||
renderedPersistents: [],
|
||||
unrenderedPersistents: [
|
||||
{pathname: '/timelines/home', component: HomeTimeline},
|
||||
{pathname: '/timelines/public', component: PublicTimeline},
|
||||
{pathname: '/timelines/public/local', component: CommunityTimeline},
|
||||
|
||||
{pathname: '/notifications', component: Notifications},
|
||||
{pathname: '/favourites', component: FavouritedStatuses}
|
||||
],
|
||||
};
|
||||
}, () => {
|
||||
if (this.unlistenHistory) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unlistenHistory = browserHistory.listen(location => {
|
||||
const pathname = location.pathname.replace(/\/$/, '').toLowerCase();
|
||||
|
||||
this.setState(oldState => {
|
||||
let persistentMatched = false;
|
||||
|
||||
const newState = {
|
||||
renderedPersistents: oldState.renderedPersistents.map(persistent => {
|
||||
const givenMatched = persistent.pathname === pathname;
|
||||
|
||||
if (givenMatched) {
|
||||
persistentMatched = true;
|
||||
}
|
||||
|
||||
return {
|
||||
hidden: !givenMatched,
|
||||
pathname: persistent.pathname,
|
||||
component: persistent.component
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
if (!persistentMatched) {
|
||||
newState.unrenderedPersistents = [];
|
||||
|
||||
oldState.unrenderedPersistents.forEach(persistent => {
|
||||
if (persistent.pathname === pathname) {
|
||||
persistentMatched = true;
|
||||
|
||||
newState.renderedPersistents.push({
|
||||
hidden: false,
|
||||
pathname: persistent.pathname,
|
||||
component: persistent.component
|
||||
});
|
||||
} else {
|
||||
newState.unrenderedPersistents.push(persistent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newState.mountImpersistent = !persistentMatched;
|
||||
|
||||
return newState;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.unlistenHistory) {
|
||||
this.unlistenHistory();
|
||||
}
|
||||
|
||||
this.unlistenHistory = "done";
|
||||
}
|
||||
|
||||
render () {
|
||||
// Hide some components rather than unmounting them to allow to show again
|
||||
// quickly and keep the view state such as the scrolled offset.
|
||||
const persistentsView = this.state.renderedPersistents.map((persistent) =>
|
||||
<div aria-hidden={persistent.hidden} key={persistent.pathname} className='mastodon-column-container' style={persistent.hidden ? hiddenColumnContainerStyle : null}>
|
||||
<persistent.component shouldUpdateScroll={persistent.hidden ? Function.prototype : getTopWhenReplacing} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<UI>
|
||||
{this.state.mountImpersistent && this.props.children}
|
||||
{persistentsView}
|
||||
</UI>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Container.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
class Mastodon extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
const { locale } = this.props;
|
||||
@@ -141,14 +265,14 @@ const Mastodon = React.createClass({
|
||||
}
|
||||
|
||||
store.dispatch(showOnboardingOnce());
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (typeof this.subscription !== 'undefined') {
|
||||
this.subscription.close();
|
||||
this.subscription = null;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { locale } = this.props;
|
||||
@@ -157,18 +281,12 @@ const Mastodon = React.createClass({
|
||||
<IntlProvider locale={locale} messages={getMessagesForLocale(locale)}>
|
||||
<Provider store={store}>
|
||||
<Router history={browserHistory} render={applyRouterMiddleware(useScroll())}>
|
||||
<Route path='/' component={UI}>
|
||||
<Route path='/' component={Container}>
|
||||
<IndexRedirect to="/getting-started" />
|
||||
|
||||
<Route path='getting-started' component={GettingStarted} />
|
||||
<Route path='timelines/home' component={HomeTimeline} />
|
||||
<Route path='timelines/public' component={PublicTimeline} />
|
||||
<Route path='timelines/public/local' component={CommunityTimeline} />
|
||||
<Route path='timelines/tag/:id' component={HashtagTimeline} />
|
||||
|
||||
<Route path='notifications' component={Notifications} />
|
||||
<Route path='favourites' component={FavouritedStatuses} />
|
||||
|
||||
<Route path='statuses/new' component={Compose} />
|
||||
<Route path='statuses/:statusId' component={Status} />
|
||||
<Route path='statuses/:statusId/reblogs' component={Reblogs} />
|
||||
@@ -191,6 +309,10 @@ const Mastodon = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Mastodon.propTypes = {
|
||||
locale: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Mastodon;
|
||||
|
@@ -20,6 +20,14 @@ import { initReport } from '../actions/reports';
|
||||
import { openModal } from '../actions/modal';
|
||||
import { createSelector } from 'reselect'
|
||||
import { isMobile } from '../is_mobile'
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
@@ -34,7 +42,7 @@ const makeMapStateToProps = () => {
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onReply (status, router) {
|
||||
dispatch(replyCompose(status, router));
|
||||
@@ -65,7 +73,11 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
},
|
||||
|
||||
onDelete (status) {
|
||||
dispatch(deleteStatus(status.get('id')));
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(deleteStatus(status.get('id')))
|
||||
}));
|
||||
},
|
||||
|
||||
onMention (account, router) {
|
||||
@@ -81,7 +93,11 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
},
|
||||
|
||||
onBlock (account) {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(blockAccount(account.get('id')))
|
||||
}));
|
||||
},
|
||||
|
||||
onReport (status) {
|
||||
@@ -89,9 +105,13 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
},
|
||||
|
||||
onMute (account) {
|
||||
dispatch(muteAccount(account.get('id')));
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.muteConfirm),
|
||||
onConfirm: () => dispatch(muteAccount(account.get('id')))
|
||||
}));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(Status);
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import DropdownMenu from '../../../components/dropdown_menu';
|
||||
import { Link } from 'react-router';
|
||||
import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
@@ -17,31 +17,7 @@ const messages = defineMessages({
|
||||
disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' }
|
||||
});
|
||||
|
||||
const outerDropdownStyle = {
|
||||
padding: '10px',
|
||||
flex: '1 1 auto'
|
||||
};
|
||||
|
||||
const outerLinksStyle = {
|
||||
flex: '1 1 auto',
|
||||
display: 'flex',
|
||||
lineHeight: '18px'
|
||||
};
|
||||
|
||||
const ActionBar = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
me: React.PropTypes.number.isRequired,
|
||||
onFollow: React.PropTypes.func,
|
||||
onBlock: React.PropTypes.func.isRequired,
|
||||
onMention: React.PropTypes.func.isRequired,
|
||||
onReport: React.PropTypes.func.isRequired,
|
||||
onMute: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class ActionBar extends React.PureComponent {
|
||||
|
||||
render () {
|
||||
const { account, me, intl } = this.props;
|
||||
@@ -76,11 +52,11 @@ const ActionBar = React.createClass({
|
||||
|
||||
return (
|
||||
<div className='account__action-bar'>
|
||||
<div style={outerDropdownStyle}>
|
||||
<div className='account__action-bar-dropdown'>
|
||||
<DropdownMenu items={menu} icon='bars' size={24} direction="right" />
|
||||
</div>
|
||||
|
||||
<div style={outerLinksStyle}>
|
||||
<div className='account__action-bar-links'>
|
||||
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}>
|
||||
<span><FormattedMessage id='account.posts' defaultMessage='Posts' /></span>
|
||||
<strong><FormattedNumber value={account.get('statuses_count')} /> {extraInfo}</strong>
|
||||
@@ -100,6 +76,17 @@ const ActionBar = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
ActionBar.propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
me: PropTypes.number.isRequired,
|
||||
onFollow: PropTypes.func,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onReport: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(ActionBar);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import emojify from '../../../emoji';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
@@ -21,30 +21,28 @@ const makeMapStateToProps = () => {
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const Avatar = React.createClass({
|
||||
class Avatar extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
autoPlayGif: React.PropTypes.bool.isRequired
|
||||
},
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
|
||||
getInitialState () {
|
||||
return {
|
||||
this.state = {
|
||||
isHovered: false
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
this.handleMouseOver = this.handleMouseOver.bind(this);
|
||||
this.handleMouseOut = this.handleMouseOut.bind(this);
|
||||
}
|
||||
|
||||
handleMouseOver () {
|
||||
if (this.state.isHovered) return;
|
||||
this.setState({ isHovered: true });
|
||||
},
|
||||
}
|
||||
|
||||
handleMouseOut () {
|
||||
if (!this.state.isHovered) return;
|
||||
this.setState({ isHovered: false });
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, autoPlayGif } = this.props;
|
||||
@@ -58,7 +56,7 @@ const Avatar = React.createClass({
|
||||
className='account__header__avatar'
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden', backgroundSize: '90px 90px', backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }}
|
||||
style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }}
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
onFocus={this.handleMouseOver}
|
||||
@@ -69,19 +67,14 @@ const Avatar = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
const Header = React.createClass({
|
||||
Avatar.propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
autoPlayGif: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
propTypes: {
|
||||
account: ImmutablePropTypes.map,
|
||||
me: React.PropTypes.number.isRequired,
|
||||
onFollow: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
autoPlayGif: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class Header extends React.Component {
|
||||
|
||||
render () {
|
||||
const { account, me, intl } = this.props;
|
||||
@@ -142,6 +135,14 @@ const Header = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
me: PropTypes.number.isRequired,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
autoPlayGif: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default connect(makeMapStateToProps)(injectIntl(Header));
|
||||
|
@@ -1,46 +1,40 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import InnerHeader from '../../account/components/header';
|
||||
import ActionBar from '../../account/components/action_bar';
|
||||
import MissingIndicator from '../../../components/missing_indicator';
|
||||
|
||||
const Header = React.createClass({
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object
|
||||
},
|
||||
class Header extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
account: ImmutablePropTypes.map,
|
||||
me: React.PropTypes.number.isRequired,
|
||||
onFollow: React.PropTypes.func.isRequired,
|
||||
onBlock: React.PropTypes.func.isRequired,
|
||||
onMention: React.PropTypes.func.isRequired,
|
||||
onReport: React.PropTypes.func.isRequired,
|
||||
onMute: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleFollow = this.handleFollow.bind(this);
|
||||
this.handleBlock = this.handleBlock.bind(this);
|
||||
this.handleMention = this.handleMention.bind(this);
|
||||
this.handleReport = this.handleReport.bind(this);
|
||||
this.handleMute = this.handleMute.bind(this);
|
||||
}
|
||||
|
||||
handleFollow () {
|
||||
this.props.onFollow(this.props.account);
|
||||
},
|
||||
}
|
||||
|
||||
handleBlock () {
|
||||
this.props.onBlock(this.props.account);
|
||||
},
|
||||
}
|
||||
|
||||
handleMention () {
|
||||
this.props.onMention(this.props.account, this.context.router);
|
||||
},
|
||||
}
|
||||
|
||||
handleReport () {
|
||||
this.props.onReport(this.props.account);
|
||||
this.context.router.push('/report');
|
||||
},
|
||||
}
|
||||
|
||||
handleMute() {
|
||||
this.props.onMute(this.props.account);
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, me } = this.props;
|
||||
@@ -50,7 +44,7 @@ const Header = React.createClass({
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='account-timeline__header'>
|
||||
<InnerHeader
|
||||
account={account}
|
||||
me={me}
|
||||
@@ -68,6 +62,20 @@ const Header = React.createClass({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
me: PropTypes.number.isRequired,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onReport: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Header.contextTypes = {
|
||||
router: PropTypes.object
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
@@ -11,6 +11,13 @@ import {
|
||||
} from '../../../actions/accounts';
|
||||
import { mentionCompose } from '../../../actions/compose';
|
||||
import { initReport } from '../../../actions/reports';
|
||||
import { openModal } from '../../../actions/modal';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' }
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
@@ -23,7 +30,7 @@ const makeMapStateToProps = () => {
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onFollow (account) {
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
dispatch(unfollowAccount(account.get('id')));
|
||||
@@ -36,7 +43,11 @@ const mapDispatchToProps = dispatch => ({
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => dispatch(blockAccount(account.get('id')))
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -52,9 +63,13 @@ const mapDispatchToProps = dispatch => ({
|
||||
if (account.getIn(['relationship', 'muting'])) {
|
||||
dispatch(unmuteAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(muteAccount(account.get('id')));
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.muteConfirm),
|
||||
onConfirm: () => dispatch(muteAccount(account.get('id')))
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(Header);
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
fetchAccount,
|
||||
fetchAccountTimeline,
|
||||
@@ -20,36 +20,30 @@ const mapStateToProps = (state, props) => ({
|
||||
me: state.getIn(['meta', 'me'])
|
||||
});
|
||||
|
||||
const AccountTimeline = React.createClass({
|
||||
class AccountTimeline extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list,
|
||||
isLoading: React.PropTypes.bool,
|
||||
hasMore: React.PropTypes.bool,
|
||||
me: React.PropTypes.number.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleScrollToBottom = this.handleScrollToBottom.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchAccount(Number(this.props.params.accountId)));
|
||||
this.props.dispatch(fetchAccountTimeline(Number(this.props.params.accountId)));
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
|
||||
this.props.dispatch(fetchAccount(Number(nextProps.params.accountId)));
|
||||
this.props.dispatch(fetchAccountTimeline(Number(nextProps.params.accountId)));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleScrollToBottom () {
|
||||
if (!this.props.isLoading && this.props.hasMore) {
|
||||
this.props.dispatch(expandAccountTimeline(Number(this.props.params.accountId)));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { statusIds, isLoading, hasMore, me } = this.props;
|
||||
@@ -68,6 +62,7 @@ const AccountTimeline = React.createClass({
|
||||
|
||||
<StatusList
|
||||
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
|
||||
scrollKey='account_timeline'
|
||||
statusIds={statusIds}
|
||||
isLoading={isLoading}
|
||||
hasMore={hasMore}
|
||||
@@ -78,6 +73,15 @@ const AccountTimeline = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
AccountTimeline.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
me: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(AccountTimeline);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import { ScrollContainer } from 'react-router-scroll';
|
||||
import Column from '../ui/components/column';
|
||||
@@ -17,19 +17,16 @@ const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'blocks', 'items'])
|
||||
});
|
||||
|
||||
const Blocks = React.createClass({
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
class Blocks extends React.PureComponent {
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchBlocks());
|
||||
},
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
@@ -37,7 +34,7 @@ const Blocks = React.createClass({
|
||||
if (scrollTop === scrollHeight - clientHeight) {
|
||||
this.props.dispatch(expandBlocks());
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, accountIds } = this.props;
|
||||
@@ -63,6 +60,13 @@ const Blocks = React.createClass({
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Blocks.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Blocks));
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
import Column from '../ui/components/column';
|
||||
import {
|
||||
@@ -25,17 +25,7 @@ const mapStateToProps = state => ({
|
||||
|
||||
let subscription;
|
||||
|
||||
const CommunityTimeline = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||
accessToken: React.PropTypes.string.isRequired,
|
||||
hasUnread: React.PropTypes.bool
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class CommunityTimeline extends React.PureComponent {
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||
@@ -72,14 +62,14 @@ const CommunityTimeline = React.createClass({
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
// if (typeof subscription !== 'undefined') {
|
||||
// subscription.close();
|
||||
// subscription = null;
|
||||
// }
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, hasUnread } = this.props;
|
||||
@@ -87,11 +77,19 @@ const CommunityTimeline = React.createClass({
|
||||
return (
|
||||
<Column icon='users' active={hasUnread} heading={intl.formatMessage(messages.title)}>
|
||||
<ColumnBackButtonSlim />
|
||||
<StatusListContainer type='community' emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} />
|
||||
<StatusListContainer {...this.props} scrollKey='community_timeline' type='community' emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} />
|
||||
</Column>
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
CommunityTimeline.propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
streamingAPIBaseURL: PropTypes.string.isRequired,
|
||||
accessToken: PropTypes.string.isRequired,
|
||||
hasUnread: PropTypes.bool
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(CommunityTimeline));
|
||||
|
@@ -3,8 +3,8 @@ import DisplayName from '../../../components/display_name';
|
||||
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')} staticSrc={account.get('avatar_static')} size={18} /></div>
|
||||
<div className='autosuggest-account'>
|
||||
<div className='autosuggest-account-icon'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={18} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
);
|
||||
|
@@ -3,7 +3,7 @@ import DisplayName from '../../../components/display_name';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
const AutosuggestStatus = ({ status }) => (
|
||||
<div style={{ overflow: 'hidden' }} className='autosuggest-status'>
|
||||
<div className='autosuggest-status'>
|
||||
<FormattedMessage id='search.status_by' defaultMessage='Status by {name}' values={{ name: <strong>@{status.getIn(['account', 'acct'])}</strong> }} />
|
||||
</div>
|
||||
);
|
||||
|
@@ -1,27 +1,26 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import { length } from 'stringz';
|
||||
|
||||
const CharacterCounter = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
text: React.PropTypes.string.isRequired,
|
||||
max: React.PropTypes.number.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class CharacterCounter extends React.PureComponent {
|
||||
|
||||
checkRemainingText (diff) {
|
||||
if (diff <= 0) {
|
||||
return <span style={{ fontSize: '16px', cursor: 'default', color: '#ff5050' }}>{diff}</span>;
|
||||
return <span className='character-counter character-counter--over'>{diff}</span>;
|
||||
}
|
||||
return <span style={{ fontSize: '16px', cursor: 'default' }}>{diff}</span>;
|
||||
},
|
||||
return <span className='character-counter'>{diff}</span>;
|
||||
}
|
||||
|
||||
render () {
|
||||
const diff = this.props.max - this.props.text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length;
|
||||
const diff = this.props.max - length(this.props.text);
|
||||
|
||||
return this.checkRemainingText(diff);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
CharacterCounter.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
max: PropTypes.number.isRequired
|
||||
}
|
||||
|
||||
export default CharacterCounter;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import CharacterCounter from './character_counter';
|
||||
import Button from '../../../components/button';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||
import { debounce } from 'react-decoration';
|
||||
@@ -15,6 +15,7 @@ import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
||||
import EmojiPickerDropdown from './emoji_picker_dropdown';
|
||||
import UploadFormContainer from '../containers/upload_form_container';
|
||||
import TextIconButton from './text_icon_button';
|
||||
import WarningContainer from '../containers/warning_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
|
||||
@@ -22,66 +23,53 @@ const messages = defineMessages({
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }
|
||||
});
|
||||
|
||||
const ComposeForm = React.createClass({
|
||||
class ComposeForm extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
text: React.PropTypes.string.isRequired,
|
||||
suggestion_token: React.PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
spoiler: React.PropTypes.bool,
|
||||
privacy: React.PropTypes.string,
|
||||
spoiler_text: React.PropTypes.string,
|
||||
focusDate: React.PropTypes.instanceOf(Date),
|
||||
preselectDate: React.PropTypes.instanceOf(Date),
|
||||
is_submitting: React.PropTypes.bool,
|
||||
is_uploading: React.PropTypes.bool,
|
||||
me: React.PropTypes.number,
|
||||
needsPrivacyWarning: React.PropTypes.bool,
|
||||
mentionedDomains: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
onSubmit: React.PropTypes.func.isRequired,
|
||||
onClearSuggestions: React.PropTypes.func.isRequired,
|
||||
onFetchSuggestions: React.PropTypes.func.isRequired,
|
||||
onSuggestionSelected: React.PropTypes.func.isRequired,
|
||||
onChangeSpoilerText: React.PropTypes.func.isRequired,
|
||||
onPaste: React.PropTypes.func.isRequired,
|
||||
onPickEmoji: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this);
|
||||
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this);
|
||||
this.onSuggestionSelected = this.onSuggestionSelected.bind(this);
|
||||
this.handleChangeSpoilerText = this.handleChangeSpoilerText.bind(this);
|
||||
this.setAutosuggestTextarea = this.setAutosuggestTextarea.bind(this);
|
||||
this.handleEmojiPick = this.handleEmojiPick.bind(this);
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.props.onChange(e.target.value);
|
||||
},
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
this.props.onSubmit();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleSubmit () {
|
||||
this.autosuggestTextarea.textarea.style.height = "auto";
|
||||
this.props.onSubmit();
|
||||
},
|
||||
}
|
||||
|
||||
onSuggestionsClearRequested () {
|
||||
this.props.onClearSuggestions();
|
||||
},
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
onSuggestionsFetchRequested (token) {
|
||||
this.props.onFetchSuggestions(token);
|
||||
},
|
||||
}
|
||||
|
||||
onSuggestionSelected (tokenStart, token, value) {
|
||||
this._restoreCaret = null;
|
||||
this.props.onSuggestionSelected(tokenStart, token, value);
|
||||
},
|
||||
}
|
||||
|
||||
handleChangeSpoilerText (e) {
|
||||
this.props.onChangeSpoilerText(e.target.value);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
// If this is the update where we've finished uploading,
|
||||
@@ -89,7 +77,7 @@ const ComposeForm = React.createClass({
|
||||
if (!nextProps.is_uploading && this.props.is_uploading) {
|
||||
this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
// This statement does several things:
|
||||
@@ -116,58 +104,45 @@ const ComposeForm = React.createClass({
|
||||
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
setAutosuggestTextarea (c) {
|
||||
this.autosuggestTextarea = c;
|
||||
},
|
||||
}
|
||||
|
||||
handleEmojiPick (data) {
|
||||
const position = this.autosuggestTextarea.textarea.selectionStart;
|
||||
this._restoreCaret = position + data.shortname.length + 1;
|
||||
this.props.onPickEmoji(position, data);
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
|
||||
const { intl, onPaste } = this.props;
|
||||
const disabled = this.props.is_submitting;
|
||||
const text = [this.props.spoiler_text, this.props.text].join('');
|
||||
|
||||
let publishText = '';
|
||||
let privacyWarning = '';
|
||||
let reply_to_other = false;
|
||||
|
||||
if (needsPrivacyWarning) {
|
||||
privacyWarning = (
|
||||
<div className='compose-form__warning'>
|
||||
<FormattedMessage
|
||||
id='compose_form.privacy_disclaimer'
|
||||
defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}} to not leak your status?'
|
||||
values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.length }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
||||
publishText = <span><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
|
||||
publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
|
||||
} else {
|
||||
publishText = intl.formatMessage(messages.publish) + (this.props.privacy !== 'unlisted' ? '!' : '');
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '10px' }}>
|
||||
<div className='compose-form'>
|
||||
<Collapsable isVisible={this.props.spoiler} fullHeight={50}>
|
||||
<div className="spoiler-input">
|
||||
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type="text" className="spoiler-input__input" />
|
||||
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type="text" className="spoiler-input__input" id='cw-spoiler-input'/>
|
||||
</div>
|
||||
</Collapsable>
|
||||
|
||||
{privacyWarning}
|
||||
<WarningContainer />
|
||||
|
||||
<ReplyIndicatorContainer />
|
||||
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div className='compose-form__autosuggest-wrapper'>
|
||||
<AutosuggestTextarea
|
||||
ref={this.setAutosuggestTextarea}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
@@ -189,7 +164,7 @@ const ComposeForm = React.createClass({
|
||||
<UploadFormContainer />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div className='compose-form__buttons-wrapper'>
|
||||
<div className='compose-form__buttons'>
|
||||
<UploadButtonContainer />
|
||||
<PrivacyDropdownContainer />
|
||||
@@ -197,15 +172,38 @@ const ComposeForm = React.createClass({
|
||||
<SpoilerButtonContainer />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', minWidth: 0 }}>
|
||||
<div style={{ paddingTop: '10px', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={text} /></div>
|
||||
<div style={{ paddingTop: '10px', overflow: 'hidden' }}><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length > 500} block /></div>
|
||||
<div className='compose-form__publish'>
|
||||
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
|
||||
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length > 500 || (text.length !==0 && text.trim().length === 0)} block /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
ComposeForm.propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
suggestion_token: PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
spoiler: PropTypes.bool,
|
||||
privacy: PropTypes.string,
|
||||
spoiler_text: PropTypes.string,
|
||||
focusDate: PropTypes.instanceOf(Date),
|
||||
preselectDate: PropTypes.instanceOf(Date),
|
||||
is_submitting: PropTypes.bool,
|
||||
is_uploading: PropTypes.bool,
|
||||
me: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
onChangeSpoilerText: PropTypes.func.isRequired,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
onPickEmoji: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(ComposeForm);
|
||||
|
@@ -1,10 +1,19 @@
|
||||
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
|
||||
import EmojiPicker from 'emojione-picker';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }
|
||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
|
||||
people: { id: 'emoji_button.people', defaultMessage: 'People' },
|
||||
nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
|
||||
food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
|
||||
activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
|
||||
travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
|
||||
objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
|
||||
symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
|
||||
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' }
|
||||
});
|
||||
|
||||
const settings = {
|
||||
@@ -13,46 +22,93 @@ const settings = {
|
||||
imagePathPNG: '/emoji/'
|
||||
};
|
||||
|
||||
const style = {
|
||||
const dropdownStyle = {
|
||||
position: 'absolute',
|
||||
right: '5px',
|
||||
top: '5px'
|
||||
};
|
||||
|
||||
const EmojiPickerDropdown = React.createClass({
|
||||
const dropdownTriggerStyle = {
|
||||
display: 'block',
|
||||
fontSize: '24px',
|
||||
lineHeight: '24px',
|
||||
marginLeft: '2px',
|
||||
width: '24px'
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
onPickEmoji: React.PropTypes.func.isRequired
|
||||
},
|
||||
class EmojiPickerDropdown extends React.PureComponent {
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.setRef = this.setRef.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
setRef (c) {
|
||||
this.dropdown = c;
|
||||
},
|
||||
}
|
||||
|
||||
handleChange (data) {
|
||||
this.dropdown.hide();
|
||||
this.props.onPickEmoji(data);
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl } = this.props;
|
||||
|
||||
const categories = {
|
||||
people: {
|
||||
title: intl.formatMessage(messages.people),
|
||||
emoji: 'smile',
|
||||
},
|
||||
nature: {
|
||||
title: intl.formatMessage(messages.nature),
|
||||
emoji: 'hamster',
|
||||
},
|
||||
food: {
|
||||
title: intl.formatMessage(messages.food),
|
||||
emoji: 'pizza',
|
||||
},
|
||||
activity: {
|
||||
title: intl.formatMessage(messages.activity),
|
||||
emoji: 'soccer',
|
||||
},
|
||||
travel: {
|
||||
title: intl.formatMessage(messages.travel),
|
||||
emoji: 'earth_americas',
|
||||
},
|
||||
objects: {
|
||||
title: intl.formatMessage(messages.objects),
|
||||
emoji: 'bulb',
|
||||
},
|
||||
symbols: {
|
||||
title: intl.formatMessage(messages.symbols),
|
||||
emoji: 'clock9',
|
||||
},
|
||||
flags: {
|
||||
title: intl.formatMessage(messages.flags),
|
||||
emoji: 'flag_gb',
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown ref={this.setRef} style={style}>
|
||||
<DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)} style={{ fontSize: `24px`, width: `24px`, lineHeight: `24px`, display: 'block', marginLeft: '2px' }}>
|
||||
<Dropdown ref={this.setRef} style={dropdownStyle}>
|
||||
<DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)} style={dropdownTriggerStyle}>
|
||||
<img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" />
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownContent className='dropdown__left light'>
|
||||
<EmojiPicker emojione={settings} onChange={this.handleChange} search={true} />
|
||||
<EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} categories={categories} search={true} />
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
EmojiPickerDropdown.propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
onPickEmoji: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(EmojiPickerDropdown);
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
@@ -7,26 +6,27 @@ import Permalink from '../../../components/permalink';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
const NavigationBar = React.createClass({
|
||||
propTypes: {
|
||||
account: ImmutablePropTypes.map.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class NavigationBar extends React.PureComponent {
|
||||
|
||||
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')} animate size={40} /></Permalink>
|
||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}><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>
|
||||
<a href='/settings/profile' style={{ color: 'inherit', textDecoration: 'none' }}><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
||||
<div className='navigation-bar__profile'>
|
||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
|
||||
<strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong>
|
||||
</Permalink>
|
||||
<a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
NavigationBar.propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired
|
||||
};
|
||||
|
||||
export default NavigationBar;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
|
||||
@@ -7,7 +7,7 @@ const messages = defineMessages({
|
||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
|
||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Private' },
|
||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
||||
private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
|
||||
@@ -15,55 +15,52 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
const iconStyle = {
|
||||
lineHeight: '27px',
|
||||
height: null
|
||||
};
|
||||
height: null,
|
||||
lineHeight: '27px'
|
||||
}
|
||||
|
||||
const PrivacyDropdown = React.createClass({
|
||||
class PrivacyDropdown extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
getInitialState () {
|
||||
return {
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
open: false
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
this.handleToggle = this.handleToggle.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.onGlobalClick = this.onGlobalClick.bind(this);
|
||||
this.setRef = this.setRef.bind(this);
|
||||
}
|
||||
|
||||
handleToggle () {
|
||||
this.setState({ open: !this.state.open });
|
||||
},
|
||||
}
|
||||
|
||||
handleClick (value, e) {
|
||||
e.preventDefault();
|
||||
this.setState({ open: false });
|
||||
this.props.onChange(value);
|
||||
},
|
||||
}
|
||||
|
||||
onGlobalClick (e) {
|
||||
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
|
||||
this.setState({ open: false });
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
window.addEventListener('click', this.onGlobalClick);
|
||||
window.addEventListener('touchstart', this.onGlobalClick);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('click', this.onGlobalClick);
|
||||
window.removeEventListener('touchstart', this.onGlobalClick);
|
||||
},
|
||||
}
|
||||
|
||||
setRef (c) {
|
||||
this.node = c;
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, onChange, intl } = this.props;
|
||||
@@ -80,7 +77,7 @@ const PrivacyDropdown = React.createClass({
|
||||
|
||||
return (
|
||||
<div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}>
|
||||
<div className='privacy-dropdown__value'><IconButton icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
|
||||
<div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle}/></div>
|
||||
<div className='privacy-dropdown__dropdown'>
|
||||
{options.map(item =>
|
||||
<div role='button' tabIndex='0' key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
||||
@@ -96,6 +93,12 @@ const PrivacyDropdown = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
PrivacyDropdown.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(PrivacyDropdown);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
@@ -10,30 +10,24 @@ const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }
|
||||
});
|
||||
|
||||
const ReplyIndicator = React.createClass({
|
||||
class ReplyIndicator extends React.PureComponent {
|
||||
|
||||
contextTypes: {
|
||||
router: React.PropTypes.object
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
status: ImmutablePropTypes.map,
|
||||
onCancel: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleAccountClick = this.handleAccountClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick () {
|
||||
this.props.onCancel();
|
||||
},
|
||||
}
|
||||
|
||||
handleAccountClick (e) {
|
||||
if (e.button === 0) {
|
||||
e.preventDefault();
|
||||
this.context.router.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { status, intl } = this.props;
|
||||
@@ -46,11 +40,11 @@ const ReplyIndicator = React.createClass({
|
||||
|
||||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div style={{ overflow: 'hidden', marginBottom: '5px' }}>
|
||||
<div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
|
||||
<div className='reply-indicator__header'>
|
||||
<div className='reply-indicator__cancel'><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'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
|
||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
|
||||
<div className='reply-indicator__display-avatar'><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
</div>
|
||||
@@ -60,6 +54,16 @@ const ReplyIndicator = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
ReplyIndicator.contextTypes = {
|
||||
router: PropTypes.object
|
||||
};
|
||||
|
||||
ReplyIndicator.propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(ReplyIndicator);
|
||||
|
@@ -1,48 +1,47 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }
|
||||
});
|
||||
|
||||
const Search = React.createClass({
|
||||
class Search extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
submitted: React.PropTypes.bool,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
onSubmit: React.PropTypes.func.isRequired,
|
||||
onClear: React.PropTypes.func.isRequired,
|
||||
onShow: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleClear = this.handleClear.bind(this);
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.props.onChange(e.target.value);
|
||||
},
|
||||
}
|
||||
|
||||
handleClear (e) {
|
||||
e.preventDefault();
|
||||
this.props.onClear();
|
||||
},
|
||||
|
||||
if (this.props.value.length > 0 || this.props.submitted) {
|
||||
this.props.onClear();
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
noop () {
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
handleFocus () {
|
||||
this.props.onShow();
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, value, submitted } = this.props;
|
||||
@@ -60,14 +59,24 @@ const Search = React.createClass({
|
||||
onFocus={this.handleFocus}
|
||||
/>
|
||||
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={hasValue ? this.handleClear : this.noop}>
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
|
||||
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
|
||||
<i aria-label="Clear search" className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
|
||||
<i aria-label={intl.formatMessage(messages.placeholder)} className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Search.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
submitted: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onShow: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(Search);
|
||||
|
@@ -1,17 +1,10 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import StatusContainer from '../../../containers/status_container';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
const SearchResults = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
results: ImmutablePropTypes.map.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class SearchResults extends React.PureComponent {
|
||||
|
||||
render () {
|
||||
const { results } = this.props;
|
||||
@@ -63,6 +56,10 @@ const SearchResults = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
SearchResults.propTypes = {
|
||||
results: ImmutablePropTypes.map.isRequired
|
||||
};
|
||||
|
||||
export default SearchResults;
|
||||
|
@@ -1,31 +1,35 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const TextIconButton = React.createClass({
|
||||
class TextIconButton extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
label: React.PropTypes.string.isRequired,
|
||||
title: React.PropTypes.string,
|
||||
active: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick (e) {
|
||||
e.preventDefault();
|
||||
this.props.onClick();
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { label, title, active } = this.props;
|
||||
const { label, title, active, ariaControls } = this.props;
|
||||
|
||||
return (
|
||||
<button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} onClick={this.handleClick}>
|
||||
<button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
TextIconButton.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
ariaControls: PropTypes.string
|
||||
};
|
||||
|
||||
export default TextIconButton;
|
||||
|
@@ -1,53 +1,60 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
upload: { id: 'upload_button.label', defaultMessage: 'Add media' }
|
||||
});
|
||||
|
||||
|
||||
const iconStyle = {
|
||||
lineHeight: '27px',
|
||||
height: null
|
||||
};
|
||||
height: null,
|
||||
lineHeight: '27px'
|
||||
}
|
||||
|
||||
const UploadButton = React.createClass({
|
||||
class UploadButton extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
disabled: React.PropTypes.bool,
|
||||
onSelectFile: React.PropTypes.func.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
resetFileKey: React.PropTypes.number,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.setRef = this.setRef.bind(this);
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
if (e.target.files.length > 0) {
|
||||
this.props.onSelectFile(e.target.files);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleClick () {
|
||||
this.fileElement.click();
|
||||
},
|
||||
}
|
||||
|
||||
setRef (c) {
|
||||
this.fileElement = c;
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
const { intl, resetFileKey, disabled } = this.props;
|
||||
|
||||
return (
|
||||
<div style={this.props.style}>
|
||||
<IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} style={iconStyle} size={18} inverted />
|
||||
<div className='compose-form__upload-button'>
|
||||
<IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle}/>
|
||||
<input key={resetFileKey} ref={this.setRef} type='file' multiple={false} onChange={this.handleChange} disabled={disabled} style={{ display: 'none' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
UploadButton.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
onSelectFile: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
resetFileKey: PropTypes.number,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(UploadButton);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import UploadProgressContainer from '../containers/upload_progress_container';
|
||||
@@ -9,24 +9,16 @@ const messages = defineMessages({
|
||||
undo: { id: 'upload_form.undo', defaultMessage: 'Undo' }
|
||||
});
|
||||
|
||||
const UploadForm = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
media: ImmutablePropTypes.list.isRequired,
|
||||
onRemoveFile: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class UploadForm extends React.PureComponent {
|
||||
|
||||
render () {
|
||||
const { intl, media } = this.props;
|
||||
|
||||
const uploads = media.map(attachment =>
|
||||
<div key={attachment.get('id')} style={{ margin: '5px', flex: '1 1 0' }}>
|
||||
<div className='compose-form__upload' key={attachment.get('id')}>
|
||||
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ scale }) =>
|
||||
<div style={{ transform: `translateZ(0) scale(${scale})`, width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}>
|
||||
<div className='compose-form__upload-thumbnail' style={{ transform: `translateZ(0) scale(${scale})`, background: `url(${attachment.get('preview_url')}) no-repeat center` }}>
|
||||
<IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} />
|
||||
</div>
|
||||
}
|
||||
@@ -35,13 +27,19 @@ const UploadForm = React.createClass({
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<div className='compose-form__upload-wrapper'>
|
||||
<UploadProgressContainer />
|
||||
<div style={{ display: 'flex', padding: '5px' }}>{uploads}</div>
|
||||
<div className='compose-form__uploads-wrapper'>{uploads}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
UploadForm.propTypes = {
|
||||
media: ImmutablePropTypes.list.isRequired,
|
||||
onRemoveFile: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(UploadForm);
|
||||
|
@@ -1,15 +1,8 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const UploadProgress = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
active: React.PropTypes.bool,
|
||||
progress: React.PropTypes.number
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class UploadProgress extends React.PureComponent {
|
||||
|
||||
render () {
|
||||
const { active, progress } = this.props;
|
||||
@@ -20,11 +13,11 @@ const UploadProgress = React.createClass({
|
||||
|
||||
return (
|
||||
<div className='upload-progress'>
|
||||
<div>
|
||||
<div className='upload-progress__icon'>
|
||||
<i className='fa fa-upload' />
|
||||
</div>
|
||||
|
||||
<div style={{ flex: '1 1 auto' }}>
|
||||
<div className='upload-progress__message'>
|
||||
<FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' />
|
||||
|
||||
<div className='upload-progress__backdrop'>
|
||||
@@ -39,6 +32,11 @@ const UploadProgress = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
UploadProgress.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
progress: PropTypes.number
|
||||
};
|
||||
|
||||
export default UploadProgress;
|
||||
|
@@ -0,0 +1,25 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Warning extends React.PureComponent {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { message } = this.props;
|
||||
|
||||
return (
|
||||
<div className='compose-form__warning'>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Warning.propTypes = {
|
||||
message: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
export default Warning;
|
@@ -1,7 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ComposeForm from '../components/compose_form';
|
||||
import { uploadCompose } from '../../../actions/compose';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
changeCompose,
|
||||
submitCompose,
|
||||
@@ -12,33 +11,20 @@ import {
|
||||
insertEmojiCompose
|
||||
} from '../../../actions/compose';
|
||||
|
||||
const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
|
||||
|
||||
const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
|
||||
return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : [];
|
||||
const mapStateToProps = state => ({
|
||||
text: state.getIn(['compose', 'text']),
|
||||
suggestion_token: state.getIn(['compose', 'suggestion_token']),
|
||||
suggestions: state.getIn(['compose', 'suggestions']),
|
||||
spoiler: state.getIn(['compose', 'spoiler']),
|
||||
spoiler_text: state.getIn(['compose', 'spoiler_text']),
|
||||
privacy: state.getIn(['compose', 'privacy']),
|
||||
focusDate: state.getIn(['compose', 'focusDate']),
|
||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||
is_submitting: state.getIn(['compose', 'is_submitting']),
|
||||
is_uploading: state.getIn(['compose', 'is_uploading']),
|
||||
me: state.getIn(['compose', 'me'])
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const mentionedUsernames = getMentionedUsernames(state);
|
||||
const mentionedUsernamesWithDomains = getMentionedDomains(state);
|
||||
|
||||
return {
|
||||
text: state.getIn(['compose', 'text']),
|
||||
suggestion_token: state.getIn(['compose', 'suggestion_token']),
|
||||
suggestions: state.getIn(['compose', 'suggestions']),
|
||||
spoiler: state.getIn(['compose', 'spoiler']),
|
||||
spoiler_text: state.getIn(['compose', 'spoiler_text']),
|
||||
privacy: state.getIn(['compose', 'privacy']),
|
||||
focusDate: state.getIn(['compose', 'focusDate']),
|
||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||
is_submitting: state.getIn(['compose', 'is_submitting']),
|
||||
is_uploading: state.getIn(['compose', 'is_uploading']),
|
||||
me: state.getIn(['compose', 'me']),
|
||||
needsPrivacyWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null,
|
||||
mentionedDomains: mentionedUsernamesWithDomains
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
onChange (text) {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import TextIconButton from '../components/text_icon_button';
|
||||
import { changeComposeSensitivity } from '../../../actions/compose';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
@@ -21,14 +22,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
|
||||
});
|
||||
|
||||
const SensitiveButton = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
visible: React.PropTypes.bool,
|
||||
active: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
class SensitiveButton extends React.PureComponent {
|
||||
|
||||
render () {
|
||||
const { visible, active, onClick, intl } = this.props;
|
||||
@@ -44,6 +38,13 @@ const SensitiveButton = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
SensitiveButton.propTypes = {
|
||||
visible: PropTypes.bool,
|
||||
active: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton));
|
||||
|
@@ -4,13 +4,14 @@ import { changeComposeSpoilerness } from '../../../actions/compose';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'compose_form.spoiler', defaultMessage: 'Hide text behind content warning' }
|
||||
title: { id: 'compose_form.spoiler', defaultMessage: 'Hide text behind warning' }
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { intl }) => ({
|
||||
label: 'CW',
|
||||
title: intl.formatMessage(messages.title),
|
||||
active: state.getIn(['compose', 'spoiler'])
|
||||
active: state.getIn(['compose', 'spoiler']),
|
||||
ariaControls: 'cw-spoiler-input'
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
@@ -0,0 +1,48 @@
|
||||
import { connect } from 'react-redux';
|
||||
import Warning from '../components/warning';
|
||||
import { createSelector } from 'reselect';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
|
||||
|
||||
const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
|
||||
return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : [];
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mentionedUsernames = getMentionedUsernames(state);
|
||||
const mentionedUsernamesWithDomains = getMentionedDomains(state);
|
||||
|
||||
return {
|
||||
needsLeakWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null,
|
||||
mentionedDomains: mentionedUsernamesWithDomains,
|
||||
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked'])
|
||||
};
|
||||
};
|
||||
|
||||
const WarningWrapper = ({ needsLeakWarning, needsLockWarning, mentionedDomains }) => {
|
||||
if (needsLockWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
|
||||
} else if (needsLeakWarning) {
|
||||
return (
|
||||
<Warning
|
||||
message={<FormattedMessage
|
||||
id='compose_form.privacy_disclaimer'
|
||||
defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.'
|
||||
values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.length }}
|
||||
/>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
WarningWrapper.propTypes = {
|
||||
needsLeakWarning: PropTypes.bool,
|
||||
needsLockWarning: PropTypes.bool,
|
||||
mentionedDomains: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(WarningWrapper);
|
@@ -1,7 +1,7 @@
|
||||
import ComposeFormContainer from './containers/compose_form_container';
|
||||
import UploadFormContainer from './containers/upload_form_container';
|
||||
import NavigationContainer from './containers/navigation_container';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { mountCompose, unmountCompose } from '../../actions/compose';
|
||||
import { Link } from 'react-router';
|
||||
@@ -22,24 +22,15 @@ const mapStateToProps = state => ({
|
||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden'])
|
||||
});
|
||||
|
||||
const Compose = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
withHeader: React.PropTypes.bool,
|
||||
showSearch: React.PropTypes.bool,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class Compose extends React.PureComponent {
|
||||
|
||||
componentDidMount () {
|
||||
this.props.dispatch(mountCompose());
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.dispatch(unmountCompose());
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { withHeader, showSearch, intl } = this.props;
|
||||
@@ -49,11 +40,11 @@ const Compose = React.createClass({
|
||||
if (withHeader) {
|
||||
header = (
|
||||
<div className='drawer__header'>
|
||||
<Link title={intl.formatMessage(messages.start)} className='drawer__tab' to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link>
|
||||
<Link title={intl.formatMessage(messages.community)} className='drawer__tab' to='/timelines/public/local'><i className='fa fa-fw fa-users' /></Link>
|
||||
<Link title={intl.formatMessage(messages.public)} className='drawer__tab' to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link>
|
||||
<a title={intl.formatMessage(messages.preferences)} className='drawer__tab' href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a>
|
||||
<a title={intl.formatMessage(messages.logout)} className='drawer__tab' href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a>
|
||||
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role="img" aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link>
|
||||
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role="img" aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link>
|
||||
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role="img" aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link>
|
||||
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)}><i role="img" aria-label={intl.formatMessage(messages.preferences)} className='fa fa-fw fa-cog' /></a>
|
||||
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role="img" aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -82,6 +73,13 @@ const Compose = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Compose.propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
withHeader: PropTypes.bool,
|
||||
showSearch: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Compose));
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites';
|
||||
@@ -18,26 +18,20 @@ const mapStateToProps = state => ({
|
||||
me: state.getIn(['meta', 'me'])
|
||||
});
|
||||
|
||||
const Favourites = React.createClass({
|
||||
class Favourites extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
loaded: React.PropTypes.bool,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
me: React.PropTypes.number.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleScrollToBottom = this.handleScrollToBottom.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchFavouritedStatuses());
|
||||
},
|
||||
}
|
||||
|
||||
handleScrollToBottom () {
|
||||
this.props.dispatch(expandFavouritedStatuses());
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { statusIds, loaded, intl, me } = this.props;
|
||||
@@ -53,11 +47,20 @@ const Favourites = React.createClass({
|
||||
return (
|
||||
<Column icon='star' heading={intl.formatMessage(messages.heading)}>
|
||||
<ColumnBackButtonSlim />
|
||||
<StatusList statusIds={statusIds} me={me} onScrollToBottom={this.handleScrollToBottom} />
|
||||
<StatusList {...this.props} onScrollToBottom={this.handleScrollToBottom} />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Favourites.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
loaded: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
me: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Favourites));
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import { fetchFavourites } from '../../actions/interactions';
|
||||
@@ -12,25 +12,17 @@ const mapStateToProps = (state, props) => ({
|
||||
accountIds: state.getIn(['user_lists', 'favourited_by', Number(props.params.statusId)])
|
||||
});
|
||||
|
||||
const Favourites = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class Favourites extends React.PureComponent {
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchFavourites(Number(this.props.params.statusId)));
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||
this.props.dispatch(fetchFavourites(Number(nextProps.params.statusId)));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds } = this.props;
|
||||
@@ -56,6 +48,12 @@ const Favourites = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Favourites.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(Favourites);
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Permalink from '../../../components/permalink';
|
||||
import Avatar from '../../../components/avatar';
|
||||
@@ -11,38 +12,23 @@ const messages = defineMessages({
|
||||
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }
|
||||
});
|
||||
|
||||
const outerStyle = {
|
||||
padding: '14px 10px'
|
||||
};
|
||||
|
||||
const panelStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
padding: '10px 0'
|
||||
};
|
||||
|
||||
const btnStyle = {
|
||||
flex: '1 1 auto',
|
||||
textAlign: 'center'
|
||||
};
|
||||
|
||||
const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
|
||||
const content = { __html: emojify(account.get('note')) };
|
||||
|
||||
return (
|
||||
<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')} staticSrc={account.get('avatar_static')} size={48} /></div>
|
||||
<div className='account-authorize__wrapper'>
|
||||
<div className='account-authorize'>
|
||||
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name'>
|
||||
<div className='account-authorize__avatar'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={48} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
<div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
|
||||
<div className='account__header__content' dangerouslySetInnerHTML={content} />
|
||||
</div>
|
||||
|
||||
<div className='account--panel' style={panelStyle}>
|
||||
<div style={btnStyle}><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div>
|
||||
<div style={btnStyle}><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div>
|
||||
<div className='account--panel'>
|
||||
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div>
|
||||
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -50,9 +36,9 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
|
||||
|
||||
AccountAuthorize.propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
onAuthorize: React.PropTypes.func.isRequired,
|
||||
onReject: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
onAuthorize: PropTypes.func.isRequired,
|
||||
onReject: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(AccountAuthorize);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import { ScrollContainer } from 'react-router-scroll';
|
||||
@@ -17,19 +17,16 @@ const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'follow_requests', 'items'])
|
||||
});
|
||||
|
||||
const FollowRequests = React.createClass({
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
class FollowRequests extends React.PureComponent {
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchFollowRequests());
|
||||
},
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
@@ -37,7 +34,7 @@ const FollowRequests = React.createClass({
|
||||
if (scrollTop === scrollHeight - clientHeight) {
|
||||
this.props.dispatch(expandFollowRequests());
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, accountIds } = this.props;
|
||||
@@ -63,6 +60,13 @@ const FollowRequests = React.createClass({
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FollowRequests.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(FollowRequests));
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import {
|
||||
@@ -18,27 +18,25 @@ const mapStateToProps = (state, props) => ({
|
||||
accountIds: state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'items'])
|
||||
});
|
||||
|
||||
const Followers = React.createClass({
|
||||
class Followers extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
this.handleLoadMore = this.handleLoadMore.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchAccount(Number(this.props.params.accountId)));
|
||||
this.props.dispatch(fetchFollowers(Number(this.props.params.accountId)));
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
|
||||
this.props.dispatch(fetchAccount(Number(nextProps.params.accountId)));
|
||||
this.props.dispatch(fetchFollowers(Number(nextProps.params.accountId)));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
@@ -46,12 +44,12 @@ const Followers = React.createClass({
|
||||
if (scrollTop === scrollHeight - clientHeight) {
|
||||
this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleLoadMore (e) {
|
||||
e.preventDefault();
|
||||
this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds } = this.props;
|
||||
@@ -70,7 +68,7 @@ const Followers = React.createClass({
|
||||
|
||||
<ScrollContainer scrollKey='followers'>
|
||||
<div className='scrollable' onScroll={this.handleScroll}>
|
||||
<div>
|
||||
<div className='followers'>
|
||||
<HeaderContainer accountId={this.props.params.accountId} />
|
||||
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
|
||||
<LoadMore onClick={this.handleLoadMore} />
|
||||
@@ -81,6 +79,12 @@ const Followers = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Followers.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(Followers);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import {
|
||||
@@ -18,27 +18,25 @@ const mapStateToProps = (state, props) => ({
|
||||
accountIds: state.getIn(['user_lists', 'following', Number(props.params.accountId), 'items'])
|
||||
});
|
||||
|
||||
const Following = React.createClass({
|
||||
class Following extends React.PureComponent {
|
||||
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
this.handleLoadMore = this.handleLoadMore.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchAccount(Number(this.props.params.accountId)));
|
||||
this.props.dispatch(fetchFollowing(Number(this.props.params.accountId)));
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
|
||||
this.props.dispatch(fetchAccount(Number(nextProps.params.accountId)));
|
||||
this.props.dispatch(fetchFollowing(Number(nextProps.params.accountId)));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
@@ -46,12 +44,12 @@ const Following = React.createClass({
|
||||
if (scrollTop === scrollHeight - clientHeight) {
|
||||
this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleLoadMore (e) {
|
||||
e.preventDefault();
|
||||
this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds } = this.props;
|
||||
@@ -70,7 +68,7 @@ const Following = React.createClass({
|
||||
|
||||
<ScrollContainer scrollKey='following'>
|
||||
<div className='scrollable' onScroll={this.handleScroll}>
|
||||
<div>
|
||||
<div className='following'>
|
||||
<HeaderContainer accountId={this.props.params.accountId} />
|
||||
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
|
||||
<LoadMore onClick={this.handleLoadMore} />
|
||||
@@ -81,6 +79,12 @@ const Following = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Following.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(Following);
|
||||
|
@@ -1,13 +1,17 @@
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnLink from '../ui/components/column_link';
|
||||
import ColumnSubheading from '../ui/components/column_subheading';
|
||||
import { Link } from 'react-router';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
||||
navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation'},
|
||||
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings'},
|
||||
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
@@ -30,16 +34,18 @@ const GettingStarted = ({ intl, me }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<ColumnLink icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />
|
||||
<ColumnLink icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />
|
||||
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
|
||||
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile={true}>
|
||||
<div className='getting-started__wrapper'>
|
||||
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)}/>
|
||||
<ColumnLink icon='users' hideOnMobile={true} text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />
|
||||
<ColumnLink icon='globe' hideOnMobile={true} text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />
|
||||
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
|
||||
{followRequests}
|
||||
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
|
||||
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)}/>
|
||||
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
|
||||
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
|
||||
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
||||
</div>
|
||||
|
||||
@@ -53,7 +59,7 @@ const GettingStarted = ({ intl, me }) => {
|
||||
};
|
||||
|
||||
GettingStarted.propTypes = {
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
me: ImmutablePropTypes.map.isRequired
|
||||
};
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
import Column from '../ui/components/column';
|
||||
import {
|
||||
@@ -17,17 +17,7 @@ const mapStateToProps = state => ({
|
||||
accessToken: state.getIn(['meta', 'access_token'])
|
||||
});
|
||||
|
||||
const HashtagTimeline = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||
accessToken: React.PropTypes.string.isRequired,
|
||||
hasUnread: React.PropTypes.bool
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class HashtagTimeline extends React.PureComponent {
|
||||
|
||||
_subscribe (dispatch, id) {
|
||||
const { streamingAPIBaseURL, accessToken } = this.props;
|
||||
@@ -46,14 +36,14 @@ const HashtagTimeline = React.createClass({
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_unsubscribe () {
|
||||
if (typeof this.subscription !== 'undefined') {
|
||||
this.subscription.close();
|
||||
this.subscription = null;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
@@ -61,7 +51,7 @@ const HashtagTimeline = React.createClass({
|
||||
|
||||
dispatch(refreshTimeline('tag', id));
|
||||
this._subscribe(dispatch, id);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.id !== this.props.params.id) {
|
||||
@@ -69,11 +59,11 @@ const HashtagTimeline = React.createClass({
|
||||
this._unsubscribe();
|
||||
this._subscribe(this.props.dispatch, nextProps.params.id);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._unsubscribe();
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { id, hasUnread } = this.props.params;
|
||||
@@ -81,11 +71,19 @@ const HashtagTimeline = React.createClass({
|
||||
return (
|
||||
<Column icon='hashtag' active={hasUnread} heading={id}>
|
||||
<ColumnBackButtonSlim />
|
||||
<StatusListContainer type='tag' id={id} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} />
|
||||
<StatusListContainer scrollKey='hashtag_timeline' type='tag' id={id} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} />
|
||||
</Column>
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
HashtagTimeline.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
streamingAPIBaseURL: PropTypes.string.isRequired,
|
||||
accessToken: PropTypes.string.isRequired,
|
||||
hasUnread: PropTypes.bool
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(HashtagTimeline);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ColumnCollapsable from '../../../components/column_collapsable';
|
||||
@@ -10,51 +10,27 @@ const messages = defineMessages({
|
||||
settings: { id: 'home.settings', defaultMessage: 'Column settings' }
|
||||
});
|
||||
|
||||
const outerStyle = {
|
||||
padding: '15px'
|
||||
};
|
||||
|
||||
const sectionStyle = {
|
||||
cursor: 'default',
|
||||
display: 'block',
|
||||
fontWeight: '500',
|
||||
marginBottom: '10px'
|
||||
};
|
||||
|
||||
const rowStyle = {
|
||||
|
||||
};
|
||||
|
||||
const ColumnSettings = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
onSave: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class ColumnSettings extends React.PureComponent {
|
||||
|
||||
render () {
|
||||
const { settings, onChange, onSave, intl } = this.props;
|
||||
|
||||
return (
|
||||
<ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={209} onCollapse={onSave}>
|
||||
<div className='column-settings--outer' style={outerStyle}>
|
||||
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
||||
<div className='column-settings__outer'>
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
||||
|
||||
<div style={rowStyle}>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
|
||||
</div>
|
||||
|
||||
<div style={rowStyle}>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||
</div>
|
||||
|
||||
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||
|
||||
<div style={rowStyle}>
|
||||
<div className='column-settings__row'>
|
||||
<SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,6 +38,13 @@ const ColumnSettings = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
ColumnSettings.propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
export default injectIntl(ColumnSettings);
|
||||
|
@@ -1,33 +1,22 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
const style = {
|
||||
display: 'block',
|
||||
fontFamily: 'inherit',
|
||||
marginBottom: '10px',
|
||||
padding: '7px 0',
|
||||
boxSizing: 'border-box',
|
||||
width: '100%'
|
||||
};
|
||||
class SettingText extends React.PureComponent {
|
||||
|
||||
const SettingText = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
settingKey: React.PropTypes.array.isRequired,
|
||||
label: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
},
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.props.onChange(this.props.settingKey, e.target.value)
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { settings, settingKey, label } = this.props;
|
||||
|
||||
return (
|
||||
<input
|
||||
style={style}
|
||||
className='setting-text'
|
||||
value={settings.getIn(settingKey)}
|
||||
onChange={this.handleChange}
|
||||
@@ -36,6 +25,13 @@ const SettingText = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
SettingText.propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
settingKey: PropTypes.array.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SettingText;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
import Column from '../ui/components/column';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
@@ -14,14 +14,7 @@ const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0
|
||||
});
|
||||
|
||||
const HomeTimeline = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
hasUnread: React.PropTypes.bool
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
class HomeTimeline extends React.PureComponent {
|
||||
|
||||
render () {
|
||||
const { intl, hasUnread } = this.props;
|
||||
@@ -29,11 +22,16 @@ const HomeTimeline = React.createClass({
|
||||
return (
|
||||
<Column icon='home' active={hasUnread} heading={intl.formatMessage(messages.title)}>
|
||||
<ColumnSettingsContainer />
|
||||
<StatusListContainer {...this.props} type='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage="You aren't following anyone yet. Visit {public} or use search to get started and meet other users." values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} />
|
||||
<StatusListContainer {...this.props} scrollKey='home_timeline' type='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage="You aren't following anyone yet. Visit {public} or use search to get started and meet other users." values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} />
|
||||
</Column>
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
HomeTimeline.propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasUnread: PropTypes.bool
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(HomeTimeline));
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import { ScrollContainer } from 'react-router-scroll';
|
||||
@@ -17,19 +17,16 @@ const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'mutes', 'items'])
|
||||
});
|
||||
|
||||
const Mutes = React.createClass({
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
class Mutes extends React.PureComponent {
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchMutes());
|
||||
},
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
@@ -37,7 +34,7 @@ const Mutes = React.createClass({
|
||||
if (scrollTop === scrollHeight - clientHeight) {
|
||||
this.props.dispatch(expandMutes());
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, accountIds } = this.props;
|
||||
@@ -54,7 +51,7 @@ const Mutes = React.createClass({
|
||||
<Column icon='volume-off' heading={intl.formatMessage(messages.heading)}>
|
||||
<ColumnBackButtonSlim />
|
||||
<ScrollContainer scrollKey='mutes'>
|
||||
<div className='scrollable' onScroll={this.handleScroll}>
|
||||
<div className='scrollable mutes' onScroll={this.handleScroll}>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} />
|
||||
)}
|
||||
@@ -63,6 +60,14 @@ const Mutes = React.createClass({
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Mutes.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Mutes));
|
||||
|