Updating to currentclosed-social-glitch-2
@ -0,0 +1 @@ | |||
VAGRANT=true |
@ -0,0 +1,2 @@ | |||
web: bundle exec puma -C config/puma.rb | |||
worker: bundle exec sidekiq -q default -q mailers -q push |
@ -0,0 +1,109 @@ | |||
# -*- mode: ruby -*- | |||
# vi: set ft=ruby : | |||
$provision = <<SCRIPT | |||
cd /vagrant # This is where the host folder/repo is mounted | |||
# Add the yarn repo + yarn repo keys | |||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - | |||
sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main' | |||
# Add repo for NodeJS | |||
curl -sL https://deb.nodesource.com/setup_4.x | sudo bash - | |||
# Add firewall rule to redirect 80 to 3000 and save | |||
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000 | |||
echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections | |||
echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections | |||
sudo apt-get install iptables-persistent -y | |||
# Add packages to build and run Mastodon | |||
sudo apt-get install \ | |||
git-core \ | |||
g++ \ | |||
libpq-dev \ | |||
libxml2-dev \ | |||
libxslt1-dev \ | |||
imagemagick \ | |||
nodejs \ | |||
redis-server \ | |||
redis-tools \ | |||
postgresql \ | |||
postgresql-contrib \ | |||
yarn \ | |||
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 -)" | |||
echo "Compiling Ruby 2.3.1: warning, this takes a while!!!" | |||
rbenv install 2.3.1 | |||
rbenv global 2.3.1 | |||
cd /vagrant | |||
# Configure database | |||
sudo -u postgres createuser -U postgres vagrant -s | |||
sudo -u postgres createdb -U postgres mastodon_development | |||
# Install gems and node modules | |||
gem install bundler | |||
bundle install | |||
yarn install | |||
# Build Mastodon | |||
bundle exec rails db:setup | |||
bundle exec rails assets:precompile | |||
SCRIPT | |||
$start = <<SCRIPT | |||
cd /vagrant | |||
export $(cat ".env.vagrant" | xargs) | |||
rails s -d -b 0.0.0.0 | |||
SCRIPT | |||
VAGRANTFILE_API_VERSION = "2" | |||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| | |||
config.vm.box = "ubuntu/trusty64" | |||
config.vm.provider :virtualbox do |vb| | |||
vb.name = "mastodon" | |||
vb.customize ["modifyvm", :id, "--memory", "1024"] | |||
end | |||
config.vm.hostname = "mastodon.dev" | |||
# This uses the vagrant-hostsupdater plugin, and lets you | |||
# access the development site at http://mastodon.dev. | |||
# To install: | |||
# $ vagrant plugin install hostsupdater | |||
if defined?(VagrantPlugins::HostsUpdater) | |||
config.vm.network :private_network, ip: "192.168.42.42" | |||
config.hostsupdater.remove_on_suspend = false | |||
end | |||
# Otherwise, you can access the site at http://localhost:3000 | |||
config.vm.network :forwarded_port, guest: 80, host: 3000 | |||
# Full provisioning script, only runs on first 'vagrant up' or with 'vagrant provision' | |||
config.vm.provision :shell, inline: $provision, privileged: false | |||
# Start up script, runs on every 'vagrant up' | |||
config.vm.provision :shell, inline: $start, run: 'always', privileged: false | |||
end |
@ -0,0 +1,91 @@ | |||
{ | |||
"name": "Mastodon", | |||
"description": "A GNU Social-compatible microblogging server", | |||
"repository": "https://github.com/tootsuite/mastodon", | |||
"logo": "https://github.com/tootsuite/mastodon/raw/master/app/assets/images/logo.png", | |||
"env": { | |||
"HEROKU": { | |||
"description": "Leave this as true", | |||
"value": "true", | |||
"required": true | |||
}, | |||
"LOCAL_DOMAIN": { | |||
"description": "The domain that your Mastodon instance will run on (this can be appname.herokuapp.com or a custom domain)", | |||
"required": true | |||
}, | |||
"LOCAL_HTTPS": { | |||
"description": "Will your domain support HTTPS? (Automatic for herokuapp, requires manual configuration for custom domains)", | |||
"value": "false", | |||
"required": true | |||
}, | |||
"PAPERCLIP_SECRET": { | |||
"description": "The secret key for storing media files", | |||
"generator": "secret" | |||
}, | |||
"SECRET_KEY_BASE": { | |||
"description": "The secret key base", | |||
"generator": "secret" | |||
}, | |||
"SINGLE_USER_MODE": { | |||
"description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)", | |||
"value": "false", | |||
"required": true | |||
}, | |||
"S3_ENABLED": { | |||
"description": "Should Mastodon use Amazon S3 for storage? This is highly recommended, as Heroku does not have persistent file storage (files will be lost).", | |||
"value": "true", | |||
"required": false | |||
}, | |||
"S3_BUCKET": { | |||
"description": "Amazon S3 Bucket", | |||
"required": false | |||
}, | |||
"S3_REGION": { | |||
"description": "Amazon S3 region that the bucket is located in", | |||
"required": false | |||
}, | |||
"AWS_ACCESS_KEY_ID": { | |||
"description": "Amazon S3 Access Key", | |||
"required": false | |||
}, | |||
"AWS_SECRET_ACCESS_KEY": { | |||
"description": "Amazon S3 Secret Key", | |||
"required": false | |||
}, | |||
"SMTP_SERVER": { | |||
"description": "Hostname for SMTP server, if you want to enable email", | |||
"required": false | |||
}, | |||
"SMTP_PORT": { | |||
"description": "Port for SMTP server", | |||
"required": false | |||
}, | |||
"SMTP_LOGIN": { | |||
"description": "Username for SMTP server", | |||
"required": false | |||
}, | |||
"SMTP_PASSWORD": { | |||
"description": "Password for SMTP server", | |||
"required": false | |||
}, | |||
"SMTP_DOMAIN": { | |||
"description": "Domain for SMTP server. Will default to instance domain if blank.", | |||
"required": false | |||
} | |||
}, | |||
"buildpacks": [ | |||
{ | |||
"url": "heroku/nodejs" | |||
}, | |||
{ | |||
"url": "heroku/ruby" | |||
} | |||
], | |||
"scripts": { | |||
"postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed" | |||
}, | |||
"addons": [ | |||
"heroku-postgresql", | |||
"heroku-redis" | |||
] | |||
} |
@ -1,3 +1,8 @@ | |||
//= require jquery | |||
//= require jquery_ujs | |||
//= require extras | |||
//= require best_in_place | |||
$(function () { | |||
$(".best_in_place").best_in_place(); | |||
}); |
@ -0,0 +1,47 @@ | |||
import api from '../api'; | |||
export const STATUS_CARD_FETCH_REQUEST = 'STATUS_CARD_FETCH_REQUEST'; | |||
export const STATUS_CARD_FETCH_SUCCESS = 'STATUS_CARD_FETCH_SUCCESS'; | |||
export const STATUS_CARD_FETCH_FAIL = 'STATUS_CARD_FETCH_FAIL'; | |||
export function fetchStatusCard(id) { | |||
return (dispatch, getState) => { | |||
dispatch(fetchStatusCardRequest(id)); | |||
api(getState).get(`/api/v1/statuses/${id}/card`).then(response => { | |||
if (!response.data.url || !response.data.title || !response.data.description) { | |||
return; | |||
} | |||
dispatch(fetchStatusCardSuccess(id, response.data)); | |||
}).catch(error => { | |||
dispatch(fetchStatusCardFail(id, error)); | |||
}); | |||
}; | |||
}; | |||
export function fetchStatusCardRequest(id) { | |||
return { | |||
type: STATUS_CARD_FETCH_REQUEST, | |||
id, | |||
skipLoading: true | |||
}; | |||
}; | |||
export function fetchStatusCardSuccess(id, card) { | |||
return { | |||
type: STATUS_CARD_FETCH_SUCCESS, | |||
id, | |||
card, | |||
skipLoading: true | |||
}; | |||
}; | |||
export function fetchStatusCardFail(id, error) { | |||
return { | |||
type: STATUS_CARD_FETCH_FAIL, | |||
id, | |||
error, | |||
skipLoading: true | |||
}; | |||
}; |
@ -0,0 +1,83 @@ | |||
import api, { getLinks } from '../api' | |||
export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; | |||
export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS'; | |||
export const FAVOURITED_STATUSES_FETCH_FAIL = 'FAVOURITED_STATUSES_FETCH_FAIL'; | |||
export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_REQUEST'; | |||
export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS'; | |||
export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL'; | |||
export function fetchFavouritedStatuses() { | |||
return (dispatch, getState) => { | |||
dispatch(fetchFavouritedStatusesRequest()); | |||
api(getState).get('/api/v1/favourites').then(response => { | |||
const next = getLinks(response).refs.find(link => link.rel === 'next'); | |||
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null)); | |||
}).catch(error => { | |||
dispatch(fetchFavouritedStatusesFail(error)); | |||
}); | |||
}; | |||
}; | |||
export function fetchFavouritedStatusesRequest() { | |||
return { | |||
type: FAVOURITED_STATUSES_FETCH_REQUEST | |||
}; | |||
}; | |||
export function fetchFavouritedStatusesSuccess(statuses, next) { | |||
return { | |||
type: FAVOURITED_STATUSES_FETCH_SUCCESS, | |||
statuses, | |||
next | |||
}; | |||
}; | |||
export function fetchFavouritedStatusesFail(error) { | |||
return { | |||
type: FAVOURITED_STATUSES_FETCH_FAIL, | |||
error | |||
}; | |||
}; | |||
export function expandFavouritedStatuses() { | |||
return (dispatch, getState) => { | |||
const url = getState().getIn(['status_lists', 'favourites', 'next'], null); | |||
if (url === null) { | |||
return; | |||
} | |||
dispatch(expandFavouritedStatusesRequest()); | |||
api(getState).get(url).then(response => { | |||
const next = getLinks(response).refs.find(link => link.rel === 'next'); | |||
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null)); | |||
}).catch(error => { | |||
dispatch(expandFavouritedStatusesFail(error)); | |||
}); | |||
}; | |||
}; | |||
export function expandFavouritedStatusesRequest() { | |||
return { | |||
type: FAVOURITED_STATUSES_EXPAND_REQUEST | |||
}; | |||
}; | |||
export function expandFavouritedStatusesSuccess(statuses, next) { | |||
return { | |||
type: FAVOURITED_STATUSES_EXPAND_SUCCESS, | |||
statuses, | |||
next | |||
}; | |||
}; | |||
export function expandFavouritedStatusesFail(error) { | |||
return { | |||
type: FAVOURITED_STATUSES_EXPAND_FAIL, | |||
error | |||
}; | |||
}; |
@ -1,8 +0,0 @@ | |||
export const ACCESS_TOKEN_SET = 'ACCESS_TOKEN_SET'; | |||
export function setAccessToken(token) { | |||
return { | |||
type: ACCESS_TOKEN_SET, | |||
token: token | |||
}; | |||
}; |
@ -0,0 +1,19 @@ | |||
import axios from 'axios'; | |||
export const SETTING_CHANGE = 'SETTING_CHANGE'; | |||
export function changeSetting(key, value) { | |||
return { | |||
type: SETTING_CHANGE, | |||
key, | |||
value | |||
}; | |||
}; | |||
export function saveSettings() { | |||
return (_, getState) => { | |||
axios.put('/api/web/settings', { | |||
data: getState().get('settings').toJS() | |||
}); | |||
}; | |||
}; |
@ -0,0 +1,17 @@ | |||
import Immutable from 'immutable'; | |||
export const STORE_HYDRATE = 'STORE_HYDRATE'; | |||
const convertState = rawState => | |||
Immutable.fromJS(rawState, (k, v) => | |||
Immutable.Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x => | |||
Number.isNaN(x * 1) ? x : x * 1)); | |||
export function hydrateStore(rawState) { | |||
const state = convertState(rawState); | |||
return { | |||
type: STORE_HYDRATE, | |||
state | |||
}; | |||
}; |
@ -0,0 +1,60 @@ | |||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||
import { Motion, spring } from 'react-motion'; | |||
const iconStyle = { | |||
fontSize: '16px', | |||
padding: '15px', | |||
position: 'absolute', | |||
right: '0', | |||
top: '-48px', | |||
cursor: 'pointer' | |||
}; | |||
const ColumnCollapsable = React.createClass({ | |||
propTypes: { | |||
icon: React.PropTypes.string.isRequired, | |||
fullHeight: React.PropTypes.number.isRequired, | |||
children: React.PropTypes.node, | |||
onCollapse: React.PropTypes.func | |||
}, | |||
getInitialState () { | |||
return { | |||
collapsed: true | |||
}; | |||
}, | |||
mixins: [PureRenderMixin], | |||
handleToggleCollapsed () { | |||
const currentState = this.state.collapsed; | |||
this.setState({ collapsed: !currentState }); | |||
if (!currentState && this.props.onCollapse) { | |||
this.props.onCollapse(); | |||
} | |||
}, | |||
render () { | |||
const { icon, fullHeight, children } = this.props; | |||
const { collapsed } = this.state; | |||
return ( | |||
<div style={{ position: 'relative' }}> | |||
<div style={{...iconStyle, color: collapsed ? '#9baec8' : '#fff', background: collapsed ? '#2f3441' : '#373b4a' }} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div> | |||
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}> | |||
{({ opacity, height }) => | |||
<div style={{ overflow: 'hidden', height: `${height}px`, opacity: opacity / 100 }}> | |||
{children} | |||
</div> | |||
} | |||
</Motion> | |||
</div> | |||
); | |||
} | |||
}); | |||
export default ColumnCollapsable; |
@ -1,15 +1,17 @@ | |||
import { FormattedMessage } from 'react-intl'; | |||
const LoadingIndicator = () => { | |||
const style = { | |||
textAlign: 'center', | |||
fontSize: '16px', | |||
fontWeight: '500', | |||
color: '#616b86', | |||
paddingTop: '120px' | |||
}; | |||
return <div style={style}><FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' /></div>; | |||
const style = { | |||
textAlign: 'center', | |||
fontSize: '16px', | |||
fontWeight: '500', | |||
color: '#616b86', | |||
paddingTop: '120px' | |||
}; | |||
const LoadingIndicator = () => ( | |||
<div style={style}> | |||
<FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' /> | |||
</div> | |||
); | |||
export default LoadingIndicator; |
@ -0,0 +1,17 @@ | |||
import { FormattedMessage } from 'react-intl'; | |||
const style = { | |||
textAlign: 'center', | |||
fontSize: '16px', | |||
fontWeight: '500', | |||
color: '#616b86', | |||
paddingTop: '120px' | |||
}; | |||
const MissingIndicator = () => ( | |||
<div style={style}> | |||
<FormattedMessage id='missing_indicator.label' defaultMessage='Not found' /> | |||
</div> | |||
); | |||
export default MissingIndicator; |
@ -1,15 +1,18 @@ | |||
import { | |||
FormattedMessage, | |||
FormattedDate, | |||
FormattedRelative | |||
} from 'react-intl'; | |||
const RelativeTimestamp = ({ timestamp }) => { | |||
return <FormattedRelative value={new Date(timestamp)} />; | |||
import { injectIntl, FormattedRelative } from 'react-intl'; | |||
const RelativeTimestamp = ({ intl, timestamp }) => { | |||
const date = new Date(timestamp); | |||
return ( | |||
<time dateTime={timestamp} title={intl.formatDate(date, { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}> | |||
<FormattedRelative value={date} /> | |||
</time> | |||
); | |||
}; | |||
RelativeTimestamp.propTypes = { | |||
intl: React.PropTypes.object.isRequired, | |||
timestamp: React.PropTypes.string.isRequired | |||
}; | |||
export default RelativeTimestamp; | |||
export default injectIntl(RelativeTimestamp); |
@ -1,26 +1,75 @@ | |||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||
import { Link } from 'react-router'; | |||
import { injectIntl, defineMessages } from 'react-intl'; | |||
const style = { | |||
const messages = defineMessages({ | |||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, | |||
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Public timeline' }, | |||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, | |||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' } | |||
}); | |||
const outerStyle = { | |||
boxSizing: 'border-box', | |||
display: 'flex', | |||
flexDirection: 'column', | |||
overflowY: 'hidden' | |||
}; | |||
const innerStyle = { | |||
boxSizing: 'border-box', | |||
background: '#454b5e', | |||
padding: '0', | |||
display: 'flex', | |||
flexDirection: 'column', | |||
overflowY: 'auto' | |||
overflowY: 'auto', | |||
flexGrow: '1' | |||
}; | |||
const tabStyle = { | |||
display: 'block', | |||
flex: '1 1 auto', | |||
padding: '15px', | |||
paddingBottom: '13px', | |||
color: '#9baec8', | |||
textDecoration: 'none', | |||
textAlign: 'center', | |||
fontSize: '16px', | |||
borderBottom: '2px solid transparent' | |||
}; | |||
const Drawer = React.createClass({ | |||
const tabActiveStyle = { | |||
color: '#2b90d9', | |||
borderBottom: '2px solid #2b90d9' | |||
}; | |||
mixins: [PureRenderMixin], | |||
const Drawer = ({ children, withHeader, intl }) => { | |||
let header = ''; | |||
render () { | |||
return ( | |||
<div className='drawer' style={style}> | |||
{this.props.children} | |||
if (withHeader) { | |||
header = ( | |||
<div className='drawer__header'> | |||
<Link title={intl.formatMessage(messages.start)} style={tabStyle} to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link> | |||
<Link title={intl.formatMessage(messages.public)} style={tabStyle} to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link> | |||
<a title={intl.formatMessage(messages.preferences)} style={tabStyle} href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a> | |||
<a title={intl.formatMessage(messages.logout)} style={tabStyle} href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a> | |||
</div> | |||
); | |||
} | |||
}); | |||
return ( | |||
<div className='drawer' style={outerStyle}> | |||
{header} | |||
<div className='drawer__inner' style={innerStyle}> | |||
{children} | |||
</div> | |||
</div> | |||
); | |||
}; | |||
Drawer.propTypes = { | |||
withHeader: React.PropTypes.bool, | |||
children: React.PropTypes.node, | |||
intl: React.PropTypes.object | |||
}; | |||
export default Drawer; | |||
export default injectIntl(Drawer); |
@ -1,8 +1,10 @@ | |||
import { connect } from 'react-redux'; | |||
import NavigationBar from '../components/navigation_bar'; | |||
const mapStateToProps = (state, props) => ({ | |||
account: state.getIn(['accounts', state.getIn(['meta', 'me'])]) | |||
}); | |||
const mapStateToProps = (state, props) => { | |||
return { | |||
account: state.getIn(['accounts', state.getIn(['meta', 'me'])]) | |||
}; | |||
}; | |||
export default connect(mapStateToProps)(NavigationBar); |
@ -0,0 +1,63 @@ | |||
import { connect } from 'react-redux'; | |||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import LoadingIndicator from '../../components/loading_indicator'; | |||
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites'; | |||
import Column from '../ui/components/column'; | |||
import StatusList from '../../components/status_list'; | |||
import ColumnBackButton from '../public_timeline/components/column_back_button'; | |||
import { defineMessages, injectIntl } from 'react-intl'; | |||
const messages = defineMessages({ | |||
heading: { id: 'column.favourites', defaultMessage: 'Favourites' } | |||
}); | |||
const mapStateToProps = state => ({ | |||
statusIds: state.getIn(['status_lists', 'favourites', 'items']), | |||
loaded: state.getIn(['status_lists', 'favourites', 'loaded']), | |||
me: state.getIn(['meta', 'me']) | |||
}); | |||
const Favourites = React.createClass({ | |||
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], | |||
componentWillMount () { | |||
this.props.dispatch(fetchFavouritedStatuses()); | |||
}, | |||
handleScrollToBottom () { | |||
this.props.dispatch(expandFavouritedStatuses()); | |||
}, | |||
render () { | |||
const { statusIds, loaded, intl, me } = this.props; | |||
if (!loaded) { | |||
return ( | |||
<Column> | |||
<LoadingIndicator /> | |||
</Column> | |||
); | |||
} | |||
return ( | |||
<Column icon='star' heading={intl.formatMessage(messages.heading)}> | |||
<ColumnBackButton /> | |||
<StatusList statusIds={statusIds} me={me} onScrollToBottom={this.handleScrollToBottom} /> | |||
</Column> | |||
); | |||
} | |||
}); | |||
export default connect(mapStateToProps)(injectIntl(Favourites)); |
@ -0,0 +1,10 @@ | |||
import Column from '../ui/components/column'; | |||
import MissingIndicator from '../../components/missing_indicator'; | |||
const GenericNotFound = () => ( | |||
<Column> | |||
<MissingIndicator /> | |||
</Column> | |||
); | |||
export default GenericNotFound; |
@ -0,0 +1,68 @@ | |||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | |||
import ColumnCollapsable from '../../../components/column_collapsable'; | |||
import SettingToggle from '../../notifications/components/setting_toggle'; | |||
import SettingText from './setting_text'; | |||
const messages = defineMessages({ | |||
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter by regular expressions' } | |||
}); | |||
const outerStyle = { | |||
background: '#373b4a', | |||
padding: '15px' | |||
}; | |||
const sectionStyle = { | |||
cursor: 'default', | |||
display: 'block', | |||
fontWeight: '500', | |||
color: '#9baec8', | |||
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], | |||
render () { | |||
const { settings, onChange, onSave, intl } = this.props; | |||
return ( | |||
<ColumnCollapsable icon='sliders' fullHeight={209} onCollapse={onSave}> | |||
<div style={outerStyle}> | |||
<span style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span> | |||
<div style={rowStyle}> | |||
<SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reblogs' />} /> | |||
</div> | |||
<div style={rowStyle}> | |||
<SettingToggle settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} /> | |||
</div> | |||
<span style={sectionStyle}><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> | |||
<div style={rowStyle}> | |||
<SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} /> | |||
</div> | |||
</div> | |||
</ColumnCollapsable> | |||
); | |||
} | |||
}); | |||
export default injectIntl(ColumnSettings); |
@ -0,0 +1,41 @@ | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
const style = { | |||
display: 'block', | |||
fontFamily: 'inherit', | |||
marginBottom: '10px', | |||
padding: '7px 0', | |||
boxSizing: 'border-box', | |||
width: '100%' | |||
}; | |||
const SettingText = React.createClass({ | |||
propTypes: { | |||
settings: ImmutablePropTypes.map.isRequired, | |||
settingKey: React.PropTypes.array.isRequired, | |||
label: React.PropTypes.string.isRequired, | |||
onChange: React.PropTypes.func.isRequired | |||
}, | |||
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} | |||
placeholder={label} | |||
/> | |||
); | |||
} | |||
}); | |||
export default SettingText; |
@ -0,0 +1,21 @@ | |||
import { connect } from 'react-redux'; | |||
import ColumnSettings from '../components/column_settings'; | |||
import { changeSetting, saveSettings } from '../../../actions/settings'; | |||
const mapStateToProps = state => ({ | |||
settings: state.getIn(['settings', 'home']) | |||
}); | |||
const mapDispatchToProps = dispatch => ({ | |||
onChange (key, checked) { | |||
dispatch(changeSetting(['home', ...key], checked)); | |||
}, | |||
onSave () { | |||
dispatch(saveSettings()); | |||
} | |||
}); | |||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); |
@ -0,0 +1,32 @@ | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import Toggle from 'react-toggle'; | |||
const labelStyle = { | |||
display: 'block', | |||
lineHeight: '24px', | |||
verticalAlign: 'middle' | |||
}; | |||
const labelSpanStyle = { | |||
display: 'inline-block', | |||
verticalAlign: 'middle', | |||
marginBottom: '14px', | |||
marginLeft: '8px', | |||
color: '#9baec8' | |||
}; | |||
const SettingToggle = ({ settings, settingKey, label, onChange }) => ( | |||
<label style={labelStyle}> | |||
<Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} /> | |||
<span style={labelSpanStyle}>{label}</span> | |||
</label> | |||
); | |||
SettingToggle.propTypes = { | |||
settings: ImmutablePropTypes.map.isRequired, | |||
settingKey: React.PropTypes.array.isRequired, | |||
label: React.PropTypes.node.isRequired, | |||
onChange: React.PropTypes.func.isRequired | |||
}; | |||
export default SettingToggle; |
@ -0,0 +1,100 @@ | |||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
const outerStyle = { | |||
display: 'flex', | |||
cursor: 'pointer', | |||
fontSize: '14px', | |||
border: '1px solid #363c4b', | |||
borderRadius: '4px', | |||
color: '#616b86', | |||
marginTop: '14px', | |||
textDecoration: 'none', | |||
overflow: 'hidden' | |||
}; | |||
const contentStyle = { | |||
flex: '1 1 auto', | |||
padding: '8px', | |||
paddingLeft: '14px', | |||
overflow: 'hidden' | |||
}; | |||
const titleStyle = { | |||
display: 'block', | |||
fontWeight: '500', | |||
marginBottom: '5px', | |||
color: '#d9e1e8', | |||
overflow: 'hidden', | |||
textOverflow: 'ellipsis', | |||
whiteSpace: 'nowrap' | |||
}; | |||
const descriptionStyle = { | |||
color: '#d9e1e8' | |||
}; | |||
const imageOuterStyle = { | |||
flex: '0 0 100px', | |||
background: '#373b4a' | |||
}; | |||
const imageStyle = { | |||
display: 'block', | |||
width: '100%', | |||
height: 'auto', | |||
margin: '0', | |||
borderRadius: '4px 0 0 4px' | |||
}; | |||
const hostStyle = { | |||
display: 'block', | |||
marginTop: '5px', | |||
fontSize: '13px' | |||
}; | |||
const getHostname = url => { | |||
const parser = document.createElement('a'); | |||
parser.href = url; | |||
return parser.hostname; | |||
}; | |||
const Card = React.createClass({ | |||
propTypes: { | |||
card: ImmutablePropTypes.map | |||
}, | |||
mixins: [PureRenderMixin], | |||
render () { | |||
const { card } = this.props; | |||
if (card === null) { | |||
return null; | |||
} | |||
let image = ''; | |||
if (card.get('image')) { | |||
image = ( | |||
<div style={imageOuterStyle}> | |||
<img src={card.get('image')} alt={card.get('title')} style={imageStyle} /> | |||
</div> | |||
); | |||
} | |||
return ( | |||
<a style={outerStyle} href={card.get('url')} className='status-card'> | |||
{image} | |||
<div style={contentStyle}> | |||
<strong style={titleStyle} title={card.get('title')}>{card.get('title')}</strong> | |||
<p style={descriptionStyle}>{card.get('description').substring(0, 50)}</p> | |||
<span style={hostStyle}>{getHostname(card.get('url'))}</span> | |||
</div> | |||
</a> | |||
); | |||
} | |||
}); | |||
export default Card; |
@ -0,0 +1,8 @@ | |||
import { connect } from 'react-redux'; | |||
import Card from '../components/card'; | |||
const mapStateToProps = (state, { statusId }) => ({ | |||
card: state.getIn(['cards', statusId], null) | |||
}); | |||
export default connect(mapStateToProps)(Card); |
@ -0,0 +1,5 @@ | |||
const LAYOUT_BREAKPOINT = 1024; | |||
export function isMobile(width) { | |||
return width <= LAYOUT_BREAKPOINT; | |||
}; |
@ -0,0 +1,25 @@ | |||
import { showLoading, hideLoading } from 'react-redux-loading-bar'; | |||
const defaultTypeSuffixes = ['PENDING', 'FULFILLED', 'REJECTED']; | |||
export default function loadingBarMiddleware(config = {}) { | |||
const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes; | |||
return ({ dispatch }) => next => (action) => { | |||
if (action.type && !action.skipLoading) { | |||
const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes; | |||
const isPending = new RegExp(`${PENDING}$`, 'g'); | |||
const isFulfilled = new RegExp(`${FULFILLED}$`, 'g'); | |||
const isRejected = new RegExp(`${REJECTED}$`, 'g'); | |||
if (action.type.match(isPending)) { | |||
dispatch(showLoading()); | |||
} else if (action.type.match(isFulfilled) || action.type.match(isRejected)) { | |||
dispatch(hideLoading()); | |||
} | |||
} | |||
return next(action); | |||
}; | |||
}; |
@ -0,0 +1,14 @@ | |||
import { STATUS_CARD_FETCH_SUCCESS } from '../actions/cards'; | |||
import Immutable from 'immutable'; | |||
const initialState = Immutable.Map(); | |||
export default function cards(state = initialState, action) { | |||
switch(action.type) { | |||
case STATUS_CARD_FETCH_SUCCESS: | |||
return state.set(action.id, Immutable.fromJS(action.card)); | |||
default: | |||
return state; | |||
} | |||
}; |
@ -1,16 +1,16 @@ | |||
import { ACCESS_TOKEN_SET } from '../actions/meta'; | |||
import { ACCOUNT_SET_SELF } from '../actions/accounts'; | |||
import { STORE_HYDRATE } from '../actions/store'; | |||
import Immutable from 'immutable'; | |||
const initialState = Immutable.Map(); | |||
const initialState = Immutable.Map({ | |||
access_token: null, | |||
me: null | |||
}); | |||
export default function meta(state = initialState, action) { | |||
switch(action.type) { | |||
case ACCESS_TOKEN_SET: | |||
return state.set('access_token', action.token); | |||
case ACCOUNT_SET_SELF: | |||
return state.set('me', action.account.id); | |||
default: | |||
return state; | |||
case STORE_HYDRATE: | |||
return state.merge(action.state.get('meta')); | |||
default: | |||
return state; | |||
} | |||
}; |
@ -0,0 +1,46 @@ | |||
import { SETTING_CHANGE } from '../actions/settings'; | |||
import { STORE_HYDRATE } from '../actions/store'; | |||
import Immutable from 'immutable'; | |||
const initialState = Immutable.Map({ | |||
home: Immutable.Map({ | |||
shows: Immutable.Map({ | |||
reblog: true, | |||
reply: true | |||
}) | |||
}), | |||
notifications: Immutable.Map({ | |||
alerts: Immutable.Map({ | |||
follow: true, | |||
favourite: true, | |||
reblog: true, | |||
mention: true | |||
}), | |||
shows: Immutable.Map({ | |||
follow: true, | |||
favourite: true, | |||
reblog: true, | |||
mention: true | |||
}), | |||
sounds: Immutable.Map({ | |||
follow: true, | |||
favourite: true, | |||
reblog: true, | |||
mention: true | |||
}) | |||
}) | |||
}); | |||
export default function settings(state = initialState, action) { | |||
switch(action.type) { | |||
case STORE_HYDRATE: | |||
return state.mergeDeep(action.state.get('settings')); | |||
case SETTING_CHANGE: | |||
return state.setIn(action.key, action.value); | |||
default: | |||
return state; | |||
} | |||
}; |
@ -0,0 +1,39 @@ | |||
import { | |||
FAVOURITED_STATUSES_FETCH_SUCCESS, | |||
FAVOURITED_STATUSES_EXPAND_SUCCESS | |||
} from '../actions/favourites'; | |||
import Immutable from 'immutable'; | |||
const initialState = Immutable.Map({ | |||
favourites: Immutable.Map({ | |||
next: null, | |||
loaded: false, | |||
items: Immutable.List() | |||
}) | |||
}); | |||
const normalizeList = (state, listType, statuses, next) => { | |||
return state.update(listType, listMap => listMap.withMutations(map => { | |||
map.set('next', next); | |||
map.set('loaded', true); | |||
map.set('items', Immutable.List(statuses.map(item => item.id))); | |||
})); | |||
}; | |||
const appendToList = (state, listType, statuses, next) => { | |||
return state.update(listType, listMap => listMap.withMutations(map => { | |||
map.set('next', next); | |||
map.set('items', map.get('items').push(...statuses.map(item => item.id))); | |||
})); | |||
}; | |||
export default function statusLists(state = initialState, action) { | |||
switch(action.type) { | |||
case FAVOURITED_STATUSES_FETCH_SUCCESS: | |||
return normalizeList(state, 'favourites', action.statuses, action.next); | |||
case FAVOURITED_STATUSES_EXPAND_SUCCESS: | |||
return appendToList(state, 'favourites', action.statuses, action.next); | |||
default: | |||
return state; | |||
} | |||
}; |