commit
68bdedbe5f
@ -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"
|
||||||
|
]
|
||||||
|
}
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 874 KiB |
After Width: | Height: | Size: 1.3 KiB |
@ -1,3 +1,8 @@
|
|||||||
//= require jquery
|
//= require jquery
|
||||||
//= require jquery_ujs
|
//= require jquery_ujs
|
||||||
//= require extras
|
//= 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';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const LoadingIndicator = () => {
|
const style = {
|
||||||
const style = {
|
textAlign: 'center',
|
||||||
textAlign: 'center',
|
fontSize: '16px',
|
||||||
fontSize: '16px',
|
fontWeight: '500',
|
||||||
fontWeight: '500',
|
color: '#616b86',
|
||||||
color: '#616b86',
|
paddingTop: '120px'
|
||||||
paddingTop: '120px'
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div style={style}><FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' /></div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LoadingIndicator = () => (
|
||||||
|
<div style={style}>
|
||||||
|
<FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default LoadingIndicator;
|
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 {
|
import { injectIntl, FormattedRelative } from 'react-intl';
|
||||||
FormattedMessage,
|
|
||||||
FormattedDate,
|
const RelativeTimestamp = ({ intl, timestamp }) => {
|
||||||
FormattedRelative
|
const date = new Date(timestamp);
|
||||||
} from 'react-intl';
|
|
||||||
|
return (
|
||||||
const RelativeTimestamp = ({ timestamp }) => {
|
<time dateTime={timestamp} title={intl.formatDate(date, { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}>
|
||||||
return <FormattedRelative value={new Date(timestamp)} />;
|
<FormattedRelative value={date} />
|
||||||
|
</time>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
RelativeTimestamp.propTypes = {
|
RelativeTimestamp.propTypes = {
|
||||||
|
intl: React.PropTypes.object.isRequired,
|
||||||
timestamp: React.PropTypes.string.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',
|
boxSizing: 'border-box',
|
||||||
background: '#454b5e',
|
|
||||||
padding: '0',
|
padding: '0',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
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 () {
|
if (withHeader) {
|
||||||
return (
|
header = (
|
||||||
<div className='drawer' style={style}>
|
<div className='drawer__header'>
|
||||||
{this.props.children}
|
<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>
|
</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 { connect } from 'react-redux';
|
||||||
import NavigationBar from '../components/navigation_bar';
|
import NavigationBar from '../components/navigation_bar';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => {
|
||||||
account: state.getIn(['accounts', state.getIn(['meta', 'me'])])
|
return {
|
||||||
});
|
account: state.getIn(['accounts', state.getIn(['meta', 'me'])])
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps)(NavigationBar);
|
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 { STORE_HYDRATE } from '../actions/store';
|
||||||
import { ACCOUNT_SET_SELF } from '../actions/accounts';
|
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
const initialState = Immutable.Map();
|
const initialState = Immutable.Map({
|
||||||
|
access_token: null,
|
||||||
|
me: null
|
||||||
|
});
|
||||||
|
|
||||||
export default function meta(state = initialState, action) {
|
export default function meta(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case ACCESS_TOKEN_SET:
|
case STORE_HYDRATE:
|
||||||
return state.set('access_token', action.token);
|
return state.merge(action.state.get('meta'));
|
||||||
case ACCOUNT_SET_SELF:
|
default:
|
||||||
return state.set('me', action.account.id);
|
return state;
|
||||||
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;
|
||||||
|
}
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue