Skip to content

Commit

Permalink
Merge pull request #131 from lnanase/add_announcement_manager
Browse files Browse the repository at this point in the history
お知らせ動的対応(二コフレver)
  • Loading branch information
takayamaki committed Jan 30, 2018
2 parents 411a0ab + 0c2334d commit 9fe8a43
Show file tree
Hide file tree
Showing 38 changed files with 467 additions and 101 deletions.
67 changes: 67 additions & 0 deletions app/controllers/admin/announcements_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

module Admin
class AnnouncementsController < BaseController
before_action :set_announcement, only: [:edit, :update, :destroy]
after_action :publish_updates, only: [:create, :update, :destroy]

def index
@announcements = Announcement.all
end

def new
@announcement = Announcement.new
fill_links!
end

def edit
fill_links!
end

def create
@announcement = Announcement.new(announcement_params)
if @announcement.save
redirect_to :admin_announcements
else
fill_links!
render :new
end
end

def update
if @announcement.update(announcement_params)
redirect_to :admin_announcements
else
fill_links!
render :edit
end
end

def destroy
@announcement.destroy
redirect_to admin_announcements_path
end

private

def set_announcement
@announcement = Announcement.find(params[:id])
end

def announcement_params
params.require(:announcement).permit(:body, :order, links_attributes: [:id, :text, :url]).tap do |x|
x[:links_attributes].each do |n, link|
link[:_destroy] = 1 if link[:id].present? && link[:url].blank? && link[:text].blank?
end
end
end

def fill_links!
@announcement.links << (3 - @announcement.links.length).times.map { AnnouncementLink.new }
end

def publish_updates
AnnouncementPublishWorker.perform_async
end
end
end
8 changes: 8 additions & 0 deletions app/javascript/mastodon/actions/announcements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const TOGGLE_ANNOUNCEMENTS = 'TOGGLE_ANNOUNCEMENTS';
export const UPDATE_ANNOUNCEMENTS = 'UPDATE_ANNOUNCEMENTS';

export function toggleAnnouncements() {
return {
type: TOGGLE_ANNOUNCEMENTS,
};
};
19 changes: 19 additions & 0 deletions app/javascript/mastodon/actions/commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { connectStream } from '../stream';
import { UPDATE_ANNOUNCEMENTS } from './announcements';

export function connectCommandStream(pollingRefresh = null) {
return connectStream('commands', pollingRefresh, (dispatch) => ({
onReceive(data) {
switch(data.event) {
case 'announcements':
dispatch({
type: UPDATE_ANNOUNCEMENTS,
data: JSON.parse(data.payload),
});
break;
default:
return;
}
},
}));
}
6 changes: 6 additions & 0 deletions app/javascript/mastodon/containers/mastodon.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { connectUserStream } from '../actions/streaming';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import initialState from '../initial_state';
import { connectCommandStream } from '../actions/commands';

const { localeData, messages } = getLocale();
addLocaleData(localeData);
Expand All @@ -27,6 +28,7 @@ export default class Mastodon extends React.PureComponent {

componentDidMount() {
this.disconnect = store.dispatch(connectUserStream());
this.commandDisconnect = store.dispatch(connectCommandStream());

// Desktop notifications
// Ask after 1 minute
Expand All @@ -49,6 +51,10 @@ export default class Mastodon extends React.PureComponent {
this.disconnect();
this.disconnect = null;
}
if (this.commandDisconnect) {
this.commandDisconnect();
this.commandDisconnect = null;
}
}

render () {
Expand Down
122 changes: 41 additions & 81 deletions app/javascript/mastodon/features/compose/components/announcements.js
Original file line number Diff line number Diff line change
@@ -1,96 +1,56 @@
import React from 'react';
import Immutable from 'immutable';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import Link from 'react-router-dom/Link';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from '../../../components/announcement_icon_button';
import Motion from 'react-motion/lib/Motion';
import spring from 'react-motion/lib/spring';

const Collapsable = ({ fullHeight, minHeight, isVisible, children }) => (
<Motion defaultStyle={{ height: isVisible ? fullHeight : minHeight }} style={{ height: spring(!isVisible ? minHeight : fullHeight) }}>
{({ height }) =>
<div style={{ height: `${height}px`, overflow: 'hidden' }}>
{children}
</div>
}
</Motion>
);

Collapsable.propTypes = {
fullHeight: PropTypes.number.isRequired,
minHeight: PropTypes.number.isRequired,
isVisible: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
};

const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
welcome: { id: 'welcome.message', defaultMessage: 'Welcome to {domain}!' },
});

const hashtags = Immutable.fromJS([
'Pの自己紹介',
'みんなのP名刺',
]);

class Announcements extends React.PureComponent {
export default class Announcements extends React.PureComponent {

static propTypes = {
intl: PropTypes.object.isRequired,
homeSize: PropTypes.number,
isLoading: PropTypes.bool,
};

state = {
show: false,
isLoaded: false,
visible: PropTypes.bool.isRequired,
onToggle: PropTypes.func.isRequired,
announcements: ImmutablePropTypes.list.isRequired,
};

onClick = () => {
this.setState({ show: !this.state.show });
}
nl2br (text) {
return text.split(/(\n)/g).map((line, i) => {
if (line.match(/(\n)/g)) {
return React.createElement('br', { key: i });
}
return line;
});
}

render () {
const { intl } = this.props;
const { visible, onToggle, announcements } = this.props;
const caretClass = visible ? 'fa fa-caret-down' : 'fa fa-caret-up';

return (
<ul className='announcements'>
<li>
<Collapsable isVisible={this.state.show} fullHeight={300} minHeight={20} >
<div className='announcements__body'>
<p>{ this.nl2br(intl.formatMessage(messages.welcome, { domain: document.title }))}</p>
{hashtags.map((hashtag, i) =>
<Link key={i} to={`/timelines/tag/${hashtag}`} tabIndex={this.state.show ? undefined : -1}>
#{hashtag}
</Link>
)}
</div>
</Collapsable>
<div className='announcements__icon'>
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon='caret-up' onClick={this.onClick} size={20} animate active={this.state.show} />
</div>
</li>
</ul>
<div className='announcements'>
<div className='compose__extra__header'>
<i className='fa fa-bell' />
<FormattedMessage id='announcement.title' defaultMessage='information' />
<button className='compose__extra__header__icon' onClick={onToggle} >
<i className={caretClass} />
</button>
</div>
{ visible && (
<ul>
{announcements.map((announcement, idx) => (
<li key={idx}>
<div className='announcements__body'>
<p dangerouslySetInnerHTML={{ __html: announcement.get('body') }} />
<div className='links'>
{announcement.get('links').map((link, i) => {
if (link.get('url').indexOf('/') === 0) {
return (
<Link to={link.get('url')} key={i}>{link.get('text')}</Link>
);
} else {
return (
<a href={link.get('url')} target='_blank' key={i}>{link.get('text')}</a>
);
}
})}
</div>
</div>
</li>
))}
</ul>
)}
</div>
);
}

componentWillReceiveProps (nextProps) {
if (!this.state.isLoaded) {
if (!nextProps.isLoading && (nextProps.homeSize === 0 || this.props.homeSize !== nextProps.homeSize)) {
this.setState({ show: nextProps.homeSize < 5, isLoaded: true });
}
}
}

}

export default injectIntl(Announcements);
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { connect } from 'react-redux';
import Announcements from '../components/announcements';
import { toggleAnnouncements } from '../../../actions/announcements';

const mapStateToProps = state => {
return {
homeSize: state.getIn(['timelines', 'home', 'items']).size,
isLoading: state.getIn(['timelines', 'home', 'isLoading']),
};
};
const mapStateToProps = (state) => ({
visible: state.getIn(['announcements', 'visible']),
announcements: state.getIn(['announcements', 'list']),
});

export default connect(mapStateToProps)(Announcements);
const mapDispatchToProps = (dispatch) => ({
onToggle () {
dispatch(toggleAnnouncements());
},
});

export default connect(mapStateToProps, mapDispatchToProps)(Announcements);
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\S+)/i;

const mapStateToProps = state => ({
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
hashtagWarning: ['private','direct'].includes(state.getIn(['compose', 'privacy'])) && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])),
hashtagWarning: ['private', 'direct'].includes(state.getIn(['compose', 'privacy'])) && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])),
});

const WarningWrapper = ({ needsLockWarning, hashtagWarning }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const mapDispatchToProps = dispatch => ({
addFavouriteTags (tag, visibility) {
dispatch(addFavouriteTags(tag, visibility));
},

removeFavouriteTags (tag) {
dispatch(removeFavouriteTags(tag));
},
Expand Down
4 changes: 2 additions & 2 deletions app/javascript/mastodon/locales/defaultMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -493,8 +493,8 @@
{
"descriptors": [
{
"defaultMessage": "Welcome to {domain}!",
"id": "welcome.message"
"defaultMessage": "Information",
"id": "announcement.title"
}
],
"path": "app/javascript/mastodon/features/compose/components/announcements.json"
Expand Down
4 changes: 2 additions & 2 deletions app/javascript/mastodon/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"announcement.title": "Information",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again",
Expand Down Expand Up @@ -260,6 +261,5 @@
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"welcome.message": "Welcome to {domain}!\n\nLet's search followers by using lower hashtags."
"video.unmute": "Unmute sound"
}
4 changes: 2 additions & 2 deletions app/javascript/mastodon/locales/ja-IM.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"account.unmute": "もう一度話を聞く",
"account.unmute_notifications": "@{name}さんからの通知を受け取らない",
"account.view_full_profile": "全ての情報を見る",
"announcement.title": "お知らせ",
"boost_modal.combo": "次からは{combo}を押せば、これをスキップできます",
"bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
"bundle_column_error.retry": "再試行",
Expand Down Expand Up @@ -269,6 +270,5 @@
"video.mute": "ミュート",
"video.pause": "一時停止",
"video.play": "再生",
"video.unmute": "ミュートを解除する",
"welcome.message": "初めましての方へ\n\nいらっしゃいませ。{domain}は全てのアイマスを歓迎します。ローカルタイムラインで話に乗るもよし、まったく関係ない発言をするもよし。ぜひ、概ねアイマスの話しか流れてこないこのサーバを楽しんでください。\n\nフォローしたい人を探すにあたっては、下記のタグが参考になるでしょう。"
"video.unmute": "ミュートを解除する"
}
4 changes: 2 additions & 2 deletions app/javascript/mastodon/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"account.unmute": "ミュート解除",
"account.unmute_notifications": "@{name}さんからの通知を受け取らない",
"account.view_full_profile": "全ての情報を見る",
"announcement.title": "お知らせ",
"boost_modal.combo": "次からは{combo}を押せば、これをスキップできます",
"bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
"bundle_column_error.retry": "再試行",
Expand Down Expand Up @@ -269,6 +270,5 @@
"video.mute": "ミュート",
"video.pause": "一時停止",
"video.play": "再生",
"video.unmute": "ミュートを解除する",
"welcome.message": "初めましての方へ\n\nいらっしゃいませ。{domain}は全てのアイマスを歓迎します。ローカルタイムラインで話に乗るもよし、まったく関係ない発言をするもよし。ぜひ、概ねアイマスの話しか流れてこないこのサーバを楽しんでください。\n\nフォローしたい人を探すにあたっては、下記のタグが参考になるでしょう。"
"video.unmute": "ミュートを解除する"
}
24 changes: 24 additions & 0 deletions app/javascript/mastodon/reducers/announcements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { STORE_HYDRATE } from '../actions/store';
import Immutable from 'immutable';
import {
TOGGLE_ANNOUNCEMENTS,
UPDATE_ANNOUNCEMENTS,
} from '../actions/announcements';

const initialState = Immutable.Map({
visible: true,
list: Immutable.List(),
});

export default function announcements(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
return state.set('list', action.state.get('announcements'));
case UPDATE_ANNOUNCEMENTS:
return state.set('list', Immutable.fromJS(action.data));
case TOGGLE_ANNOUNCEMENTS:
return state.set('visible', !state.get('visible'));
default:
return state;
}
};
Loading

0 comments on commit 9fe8a43

Please sign in to comment.