Port front-end changes from f52c988e12
to glitch-soc
Signed-off-by: Thibaut Girka <thib@sitedethib.com>
closed-social-glitch-2
@ -0,0 +1,133 @@ | |||
import api from 'flavours/glitch/util/api'; | |||
import { normalizeAnnouncement } from './importer/normalizer'; | |||
export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST'; | |||
export const ANNOUNCEMENTS_FETCH_SUCCESS = 'ANNOUNCEMENTS_FETCH_SUCCESS'; | |||
export const ANNOUNCEMENTS_FETCH_FAIL = 'ANNOUNCEMENTS_FETCH_FAIL'; | |||
export const ANNOUNCEMENTS_UPDATE = 'ANNOUNCEMENTS_UPDATE'; | |||
export const ANNOUNCEMENTS_DISMISS = 'ANNOUNCEMENTS_DISMISS'; | |||
export const ANNOUNCEMENTS_REACTION_ADD_REQUEST = 'ANNOUNCEMENTS_REACTION_ADD_REQUEST'; | |||
export const ANNOUNCEMENTS_REACTION_ADD_SUCCESS = 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS'; | |||
export const ANNOUNCEMENTS_REACTION_ADD_FAIL = 'ANNOUNCEMENTS_REACTION_ADD_FAIL'; | |||
export const ANNOUNCEMENTS_REACTION_REMOVE_REQUEST = 'ANNOUNCEMENTS_REACTION_REMOVE_REQUEST'; | |||
export const ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS = 'ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS'; | |||
export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL = 'ANNOUNCEMENTS_REACTION_REMOVE_FAIL'; | |||
export const ANNOUNCEMENTS_REACTION_UPDATE = 'ANNOUNCEMENTS_REACTION_UPDATE'; | |||
const noOp = () => {}; | |||
export const fetchAnnouncements = (done = noOp) => (dispatch, getState) => { | |||
dispatch(fetchAnnouncementsRequest()); | |||
api(getState).get('/api/v1/announcements').then(response => { | |||
dispatch(fetchAnnouncementsSuccess(response.data.map(x => normalizeAnnouncement(x)))); | |||
}).catch(error => { | |||
dispatch(fetchAnnouncementsFail(error)); | |||
}).finally(() => { | |||
done(); | |||
}); | |||
}; | |||
export const fetchAnnouncementsRequest = () => ({ | |||
type: ANNOUNCEMENTS_FETCH_REQUEST, | |||
skipLoading: true, | |||
}); | |||
export const fetchAnnouncementsSuccess = announcements => ({ | |||
type: ANNOUNCEMENTS_FETCH_SUCCESS, | |||
announcements, | |||
skipLoading: true, | |||
}); | |||
export const fetchAnnouncementsFail= error => ({ | |||
type: ANNOUNCEMENTS_FETCH_FAIL, | |||
error, | |||
skipLoading: true, | |||
skipAlert: true, | |||
}); | |||
export const updateAnnouncements = announcement => ({ | |||
type: ANNOUNCEMENTS_UPDATE, | |||
announcement: normalizeAnnouncement(announcement), | |||
}); | |||
export const dismissAnnouncement = announcementId => (dispatch, getState) => { | |||
dispatch({ | |||
type: ANNOUNCEMENTS_DISMISS, | |||
id: announcementId, | |||
}); | |||
api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`); | |||
}; | |||
export const addReaction = (announcementId, name) => (dispatch, getState) => { | |||
dispatch(addReactionRequest(announcementId, name)); | |||
api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => { | |||
dispatch(addReactionSuccess(announcementId, name)); | |||
}).catch(err => { | |||
dispatch(addReactionFail(announcementId, name, err)); | |||
}); | |||
}; | |||
export const addReactionRequest = (announcementId, name) => ({ | |||
type: ANNOUNCEMENTS_REACTION_ADD_REQUEST, | |||
id: announcementId, | |||
name, | |||
skipLoading: true, | |||
}); | |||
export const addReactionSuccess = (announcementId, name) => ({ | |||
type: ANNOUNCEMENTS_REACTION_ADD_SUCCESS, | |||
id: announcementId, | |||
name, | |||
skipLoading: true, | |||
}); | |||
export const addReactionFail = (announcementId, name, error) => ({ | |||
type: ANNOUNCEMENTS_REACTION_ADD_FAIL, | |||
id: announcementId, | |||
name, | |||
error, | |||
skipLoading: true, | |||
}); | |||
export const removeReaction = (announcementId, name) => (dispatch, getState) => { | |||
dispatch(removeReactionRequest(announcementId, name)); | |||
api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => { | |||
dispatch(removeReactionSuccess(announcementId, name)); | |||
}).catch(err => { | |||
dispatch(removeReactionFail(announcementId, name, err)); | |||
}); | |||
}; | |||
export const removeReactionRequest = (announcementId, name) => ({ | |||
type: ANNOUNCEMENTS_REACTION_REMOVE_REQUEST, | |||
id: announcementId, | |||
name, | |||
skipLoading: true, | |||
}); | |||
export const removeReactionSuccess = (announcementId, name) => ({ | |||
type: ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS, | |||
id: announcementId, | |||
name, | |||
skipLoading: true, | |||
}); | |||
export const removeReactionFail = (announcementId, name, error) => ({ | |||
type: ANNOUNCEMENTS_REACTION_REMOVE_FAIL, | |||
id: announcementId, | |||
name, | |||
error, | |||
skipLoading: true, | |||
}); | |||
export const updateReaction = reaction => ({ | |||
type: ANNOUNCEMENTS_REACTION_UPDATE, | |||
reaction, | |||
}); |
@ -0,0 +1,395 @@ | |||
import React from 'react'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
import ReactSwipeableViews from 'react-swipeable-views'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import PropTypes from 'prop-types'; | |||
import IconButton from 'flavours/glitch/components/icon_button'; | |||
import Icon from 'flavours/glitch/components/icon'; | |||
import { defineMessages, injectIntl, FormattedMessage, FormattedDate, FormattedNumber } from 'react-intl'; | |||
import { autoPlayGif } from 'flavours/glitch/util/initial_state'; | |||
import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg'; | |||
import { mascot } from 'flavours/glitch/util/initial_state'; | |||
import unicodeMapping from 'flavours/glitch/util/emoji/emoji_unicode_mapping_light'; | |||
import classNames from 'classnames'; | |||
import EmojiPickerDropdown from 'flavours/glitch/features/emoji_picker'; | |||
const messages = defineMessages({ | |||
close: { id: 'lightbox.close', defaultMessage: 'Close' }, | |||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, | |||
next: { id: 'lightbox.next', defaultMessage: 'Next' }, | |||
}); | |||
class Content extends ImmutablePureComponent { | |||
static contextTypes = { | |||
router: PropTypes.object, | |||
}; | |||
static propTypes = { | |||
announcement: ImmutablePropTypes.map.isRequired, | |||
}; | |||
setRef = c => { | |||
this.node = c; | |||
} | |||
componentDidMount () { | |||
this._updateLinks(); | |||
this._updateEmojis(); | |||
} | |||
componentDidUpdate () { | |||
this._updateLinks(); | |||
this._updateEmojis(); | |||
} | |||
_updateEmojis () { | |||
const node = this.node; | |||
if (!node || autoPlayGif) { | |||
return; | |||
} | |||
const emojis = node.querySelectorAll('.custom-emoji'); | |||
for (var i = 0; i < emojis.length; i++) { | |||
let emoji = emojis[i]; | |||
if (emoji.classList.contains('status-emoji')) { | |||
continue; | |||
} | |||
emoji.classList.add('status-emoji'); | |||
emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); | |||
emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); | |||
} | |||
} | |||
_updateLinks () { | |||
const node = this.node; | |||
if (!node) { | |||
return; | |||
} | |||
const links = node.querySelectorAll('a'); | |||
for (var i = 0; i < links.length; ++i) { | |||
let link = links[i]; | |||
if (link.classList.contains('status-link')) { | |||
continue; | |||
} | |||
link.classList.add('status-link'); | |||
let mention = this.props.announcement.get('mentions').find(item => link.href === item.get('url')); | |||
if (mention) { | |||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false); | |||
link.setAttribute('title', mention.get('acct')); | |||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { | |||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); | |||
} else { | |||
link.setAttribute('title', link.href); | |||
link.classList.add('unhandled-link'); | |||
} | |||
link.setAttribute('target', '_blank'); | |||
link.setAttribute('rel', 'noopener noreferrer'); | |||
} | |||
} | |||
onMentionClick = (mention, e) => { | |||
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { | |||
e.preventDefault(); | |||
this.context.router.history.push(`/accounts/${mention.get('id')}`); | |||
} | |||
} | |||
onHashtagClick = (hashtag, e) => { | |||
hashtag = hashtag.replace(/^#/, ''); | |||
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { | |||
e.preventDefault(); | |||
this.context.router.history.push(`/timelines/tag/${hashtag}`); | |||
} | |||
} | |||
handleEmojiMouseEnter = ({ target }) => { | |||
target.src = target.getAttribute('data-original'); | |||
} | |||
handleEmojiMouseLeave = ({ target }) => { | |||
target.src = target.getAttribute('data-static'); | |||
} | |||
render () { | |||
const { announcement } = this.props; | |||
return ( | |||
<div | |||
className='announcements__item__content' | |||
ref={this.setRef} | |||
dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }} | |||
/> | |||
); | |||
} | |||
} | |||
const assetHost = process.env.CDN_HOST || ''; | |||
class Emoji extends React.PureComponent { | |||
static propTypes = { | |||
emoji: PropTypes.string.isRequired, | |||
emojiMap: ImmutablePropTypes.map.isRequired, | |||
hovered: PropTypes.bool.isRequired, | |||
}; | |||
render () { | |||
const { emoji, emojiMap, hovered } = this.props; | |||
if (unicodeMapping[emoji]) { | |||
const { filename, shortCode } = unicodeMapping[this.props.emoji]; | |||
const title = shortCode ? `:${shortCode}:` : ''; | |||
return ( | |||
<img | |||
draggable='false' | |||
className='emojione' | |||
alt={emoji} | |||
title={title} | |||
src={`${assetHost}/emoji/${filename}.svg`} | |||
/> | |||
); | |||
} else if (emojiMap.get(emoji)) { | |||
const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']); | |||
const shortCode = `:${emoji}:`; | |||
return ( | |||
<img | |||
draggable='false' | |||
className='emojione custom-emoji' | |||
alt={shortCode} | |||
title={shortCode} | |||
src={filename} | |||
/> | |||
); | |||
} else { | |||
return null; | |||
} | |||
} | |||
} | |||
class Reaction extends ImmutablePureComponent { | |||
static propTypes = { | |||
announcementId: PropTypes.string.isRequired, | |||
reaction: ImmutablePropTypes.map.isRequired, | |||
addReaction: PropTypes.func.isRequired, | |||
removeReaction: PropTypes.func.isRequired, | |||
emojiMap: ImmutablePropTypes.map.isRequired, | |||
}; | |||
state = { | |||
hovered: false, | |||
}; | |||
handleClick = () => { | |||
const { reaction, announcementId, addReaction, removeReaction } = this.props; | |||
if (reaction.get('me')) { | |||
removeReaction(announcementId, reaction.get('name')); | |||
} else { | |||
addReaction(announcementId, reaction.get('name')); | |||
} | |||
} | |||
handleMouseEnter = () => this.setState({ hovered: true }) | |||
handleMouseLeave = () => this.setState({ hovered: false }) | |||
render () { | |||
const { reaction } = this.props; | |||
let shortCode = reaction.get('name'); | |||
if (unicodeMapping[shortCode]) { | |||
shortCode = unicodeMapping[shortCode].shortCode; | |||
} | |||
return ( | |||
<button className={classNames('reactions-bar__item', { active: reaction.get('me') })} onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} title={`:${shortCode}:`}> | |||
<span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} /></span> | |||
<span className='reactions-bar__item__count'><FormattedNumber value={reaction.get('count')} /></span> | |||
</button> | |||
); | |||
} | |||
} | |||
class ReactionsBar extends ImmutablePureComponent { | |||
static propTypes = { | |||
announcementId: PropTypes.string.isRequired, | |||
reactions: ImmutablePropTypes.list.isRequired, | |||
addReaction: PropTypes.func.isRequired, | |||
removeReaction: PropTypes.func.isRequired, | |||
emojiMap: ImmutablePropTypes.map.isRequired, | |||
}; | |||
handleEmojiPick = data => { | |||
const { addReaction, announcementId } = this.props; | |||
addReaction(announcementId, data.native.replace(/:/g, '')); | |||
} | |||
render () { | |||
const { reactions } = this.props; | |||
const visibleReactions = reactions.filter(x => x.get('count') > 0); | |||
return ( | |||
<div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}> | |||
{visibleReactions.map(reaction => ( | |||
<Reaction | |||
key={reaction.get('name')} | |||
reaction={reaction} | |||
announcementId={this.props.announcementId} | |||
addReaction={this.props.addReaction} | |||
removeReaction={this.props.removeReaction} | |||
emojiMap={this.props.emojiMap} | |||
/> | |||
))} | |||
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={<Icon id='plus' />} /> | |||
</div> | |||
); | |||
} | |||
} | |||
class Announcement extends ImmutablePureComponent { | |||
static propTypes = { | |||
announcement: ImmutablePropTypes.map.isRequired, | |||
emojiMap: ImmutablePropTypes.map.isRequired, | |||
dismissAnnouncement: PropTypes.func.isRequired, | |||
addReaction: PropTypes.func.isRequired, | |||
removeReaction: PropTypes.func.isRequired, | |||
intl: PropTypes.object.isRequired, | |||
}; | |||
handleDismissClick = () => { | |||
const { dismissAnnouncement, announcement } = this.props; | |||
dismissAnnouncement(announcement.get('id')); | |||
} | |||
render () { | |||
const { announcement, intl } = this.props; | |||
const startsAt = announcement.get('starts_at') && new Date(announcement.get('starts_at')); | |||
const endsAt = announcement.get('ends_at') && new Date(announcement.get('ends_at')); | |||
const now = new Date(); | |||
const hasTimeRange = startsAt && endsAt; | |||
const skipYear = hasTimeRange && startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear(); | |||
const skipEndDate = hasTimeRange && startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear(); | |||
const skipTime = announcement.get('all_day'); | |||
return ( | |||
<div className='announcements__item'> | |||
<strong className='announcements__item__range'> | |||
<FormattedMessage id='announcement.announcement' defaultMessage='Announcement' /> | |||
{hasTimeRange && <span> · <FormattedDate value={startsAt} hour12={false} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} hour12={false} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /></span>} | |||
</strong> | |||
<Content announcement={announcement} /> | |||
<ReactionsBar | |||
reactions={announcement.get('reactions')} | |||
announcementId={announcement.get('id')} | |||
addReaction={this.props.addReaction} | |||
removeReaction={this.props.removeReaction} | |||
emojiMap={this.props.emojiMap} | |||
/> | |||
<IconButton title={intl.formatMessage(messages.close)} icon='times' className='announcements__item__dismiss-icon' onClick={this.handleDismissClick} /> | |||
</div> | |||
); | |||
} | |||
} | |||
export default @injectIntl | |||
class Announcements extends ImmutablePureComponent { | |||
static propTypes = { | |||
announcements: ImmutablePropTypes.list, | |||
emojiMap: ImmutablePropTypes.map.isRequired, | |||
fetchAnnouncements: PropTypes.func.isRequired, | |||
dismissAnnouncement: PropTypes.func.isRequired, | |||
addReaction: PropTypes.func.isRequired, | |||
removeReaction: PropTypes.func.isRequired, | |||
intl: PropTypes.object.isRequired, | |||
}; | |||
state = { | |||
index: 0, | |||
}; | |||
componentDidMount () { | |||
const { fetchAnnouncements } = this.props; | |||
fetchAnnouncements(); | |||
} | |||
handleChangeIndex = index => { | |||
this.setState({ index: index % this.props.announcements.size }); | |||
} | |||
handleNextClick = () => { | |||
this.setState({ index: (this.state.index + 1) % this.props.announcements.size }); | |||
} | |||
handlePrevClick = () => { | |||
this.setState({ index: (this.props.announcements.size + this.state.index - 1) % this.props.announcements.size }); | |||
} | |||
render () { | |||
const { announcements, intl } = this.props; | |||
const { index } = this.state; | |||
if (announcements.isEmpty()) { | |||
return null; | |||
} | |||
return ( | |||
<div className='announcements'> | |||
<img className='announcements__mastodon' alt='' draggable='false' src={mascot || elephantUIPlane} /> | |||
<div className='announcements__container'> | |||
<ReactSwipeableViews index={index} onChangeIndex={this.handleChangeIndex}> | |||
{announcements.map(announcement => ( | |||
<Announcement | |||
key={announcement.get('id')} | |||
announcement={announcement} | |||
emojiMap={this.props.emojiMap} | |||
dismissAnnouncement={this.props.dismissAnnouncement} | |||
addReaction={this.props.addReaction} | |||
removeReaction={this.props.removeReaction} | |||
intl={intl} | |||
/> | |||
))} | |||
</ReactSwipeableViews> | |||
<div className='announcements__pagination'> | |||
<IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.previous)} icon='chevron-left' onClick={this.handlePrevClick} size={13} /> | |||
<span>{index + 1} / {announcements.size}</span> | |||
<IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.next)} icon='chevron-right' onClick={this.handleNextClick} size={13} /> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@ -0,0 +1,21 @@ | |||
import { connect } from 'react-redux'; | |||
import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'mastodon/actions/announcements'; | |||
import Announcements from '../components/announcements'; | |||
import { createSelector } from 'reselect'; | |||
import { Map as ImmutableMap } from 'immutable'; | |||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap())); | |||
const mapStateToProps = state => ({ | |||
announcements: state.getIn(['announcements', 'items']), | |||
emojiMap: customEmojiMap(state), | |||
}); | |||
const mapDispatchToProps = dispatch => ({ | |||
fetchAnnouncements: () => dispatch(fetchAnnouncements()), | |||
dismissAnnouncement: id => dispatch(dismissAnnouncement(id)), | |||
addReaction: (id, name) => dispatch(addReaction(id, name)), | |||
removeReaction: (id, name) => dispatch(removeReaction(id, name)), | |||
}); | |||
export default connect(mapStateToProps, mapDispatchToProps)(Announcements); |
@ -0,0 +1,72 @@ | |||
import { | |||
ANNOUNCEMENTS_FETCH_REQUEST, | |||
ANNOUNCEMENTS_FETCH_SUCCESS, | |||
ANNOUNCEMENTS_FETCH_FAIL, | |||
ANNOUNCEMENTS_UPDATE, | |||
ANNOUNCEMENTS_DISMISS, | |||
ANNOUNCEMENTS_REACTION_UPDATE, | |||
ANNOUNCEMENTS_REACTION_ADD_REQUEST, | |||
ANNOUNCEMENTS_REACTION_ADD_FAIL, | |||
ANNOUNCEMENTS_REACTION_REMOVE_REQUEST, | |||
ANNOUNCEMENTS_REACTION_REMOVE_FAIL, | |||
} from '../actions/announcements'; | |||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; | |||
const initialState = ImmutableMap({ | |||
items: ImmutableList(), | |||
isLoading: false, | |||
}); | |||
const updateReaction = (state, id, name, updater) => state.update('items', list => list.map(announcement => { | |||
if (announcement.get('id') === id) { | |||
return announcement.update('reactions', reactions => { | |||
if (reactions.find(reaction => reaction.get('name') === name)) { | |||
return reactions.map(reaction => { | |||
if (reaction.get('name') === name) { | |||
return updater(reaction); | |||
} | |||
return reaction; | |||
}); | |||
} | |||
return reactions.push(updater(fromJS({ name, count: 0 }))); | |||
}); | |||
} | |||
return announcement; | |||
})); | |||
const updateReactionCount = (state, reaction) => updateReaction(state, reaction.announcement_id, reaction.name, x => x.set('count', reaction.count)); | |||
const addReaction = (state, id, name) => updateReaction(state, id, name, x => x.set('me', true).update('count', y => y + 1)); | |||
const removeReaction = (state, id, name) => updateReaction(state, id, name, x => x.set('me', false).update('count', y => y - 1)); | |||
export default function announcementsReducer(state = initialState, action) { | |||
switch(action.type) { | |||
case ANNOUNCEMENTS_FETCH_REQUEST: | |||
return state.set('isLoading', true); | |||
case ANNOUNCEMENTS_FETCH_SUCCESS: | |||
return state.withMutations(map => { | |||
map.set('items', fromJS(action.announcements)); | |||
map.set('isLoading', false); | |||
}); | |||
case ANNOUNCEMENTS_FETCH_FAIL: | |||
return state.set('isLoading', false); | |||
case ANNOUNCEMENTS_UPDATE: | |||
return state.update('items', list => list.unshift(fromJS(action.announcement)).sortBy(announcement => announcement.get('starts_at'))); | |||
case ANNOUNCEMENTS_DISMISS: | |||
return state.update('items', list => list.filterNot(announcement => announcement.get('id') === action.id)); | |||
case ANNOUNCEMENTS_REACTION_UPDATE: | |||
return updateReactionCount(state, action.reaction); | |||
case ANNOUNCEMENTS_REACTION_ADD_REQUEST: | |||
case ANNOUNCEMENTS_REACTION_REMOVE_FAIL: | |||
return addReaction(state, action.id, action.name); | |||
case ANNOUNCEMENTS_REACTION_REMOVE_REQUEST: | |||
case ANNOUNCEMENTS_REACTION_ADD_FAIL: | |||
return removeReaction(state, action.id, action.name); | |||
default: | |||
return state; | |||
} | |||
}; |
@ -0,0 +1,212 @@ | |||
.announcements__item__content { | |||
word-wrap: break-word; | |||
.emojione { | |||
width: 20px; | |||
height: 20px; | |||
margin: -3px 0 0; | |||
} | |||
p { | |||
margin-bottom: 10px; | |||
white-space: pre-wrap; | |||
&:last-child { | |||
margin-bottom: 0; | |||
} | |||
} | |||
a { | |||
color: $highlight-text-color; | |||
text-decoration: none; | |||
&:hover { | |||
text-decoration: underline; | |||
} | |||
&.mention { | |||
&:hover { | |||
text-decoration: none; | |||
span { | |||
text-decoration: underline; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
.announcements { | |||
background: lighten($ui-base-color, 4%); | |||
border-top: 1px solid $ui-base-color; | |||
font-size: 13px; | |||
display: flex; | |||
align-items: flex-end; | |||
&__mastodon { | |||
width: 124px; | |||
flex: 0 0 auto; | |||
@media screen and (max-width: 124px + 300px) { | |||
display: none; | |||
} | |||
} | |||
&__container { | |||
width: calc(100% - 124px); | |||
flex: 0 0 auto; | |||
position: relative; | |||
@media screen and (max-width: 124px + 300px) { | |||
width: 100%; | |||
} | |||
} | |||
&__item { | |||
box-sizing: border-box; | |||
width: 100%; | |||
padding: 15px; | |||
padding-right: 15px + 18px; | |||
position: relative; | |||
&__range { | |||
display: block; | |||
font-weight: 500; | |||
margin-bottom: 10px; | |||
} | |||
&__dismiss-icon { | |||
position: absolute; | |||
top: 12px; | |||
right: 12px; | |||
} | |||
} | |||
&__pagination { | |||
padding: 15px; | |||
color: $darker-text-color; | |||
position: absolute; | |||
bottom: 3px; | |||
right: 0; | |||
} | |||
} | |||
.layout-multiple-columns .announcements__mastodon { | |||
display: none; | |||
} | |||
.layout-multiple-columns .announcements__container { | |||
width: 100%; | |||
} | |||
.reactions-bar { | |||
display: flex; | |||
flex-wrap: wrap; | |||
align-items: center; | |||
margin-top: 15px; | |||
margin-left: -2px; | |||
width: calc(100% - (90px - 33px)); | |||
&__item { | |||
flex-shrink: 0; | |||
background: lighten($ui-base-color, 12%); | |||
border: 0; | |||
border-radius: 3px; | |||
margin: 2px; | |||
cursor: pointer; | |||
user-select: none; | |||
padding: 0 6px; | |||
display: flex; | |||
align-items: center; | |||
transition: all 100ms ease-in; | |||
transition-property: background-color, color; | |||
&__emoji { | |||
display: block; | |||
margin: 3px 0; | |||
width: 16px; | |||
height: 16px; | |||
img { | |||
display: block; | |||
margin: 0; | |||
width: 100%; | |||
height: 100%; | |||
min-width: auto; | |||
min-height: auto; | |||
vertical-align: bottom; | |||
object-fit: contain; | |||
} | |||
} | |||
&__count { | |||
display: block; | |||
min-width: 9px; | |||
font-size: 13px; | |||
font-weight: 500; | |||
text-align: center; | |||
margin-left: 6px; | |||
color: $darker-text-color; | |||
} | |||
&:hover, | |||
&:focus, | |||
&:active { | |||
background: lighten($ui-base-color, 16%); | |||
transition: all 200ms ease-out; | |||
transition-property: background-color, color; | |||
&__count { | |||
color: lighten($darker-text-color, 4%); | |||
} | |||
} | |||
&.active { | |||
transition: all 100ms ease-in; | |||
transition-property: background-color, color; | |||
background-color: mix(lighten($ui-base-color, 12%), $ui-highlight-color, 90%); | |||
.reactions-bar__item__count { | |||
color: $highlight-text-color; | |||
} | |||
} | |||
} | |||
.emoji-picker-dropdown { | |||
margin: 2px; | |||
} | |||
&:hover .emoji-button { | |||
opacity: 0.85; | |||
} | |||
.emoji-button { | |||
color: $darker-text-color; | |||
margin: 0; | |||
font-size: 16px; | |||
width: auto; | |||
flex-shrink: 0; | |||
padding: 0 6px; | |||
height: 22px; | |||
display: flex; | |||
align-items: center; | |||
opacity: 0.5; | |||
transition: all 100ms ease-in; | |||
transition-property: background-color, color; | |||
&:hover, | |||
&:active, | |||
&:focus { | |||
opacity: 1; | |||
color: lighten($darker-text-color, 4%); | |||
transition: all 200ms ease-out; | |||
transition-property: background-color, color; | |||
} | |||
} | |||
&--empty { | |||
.emoji-button { | |||
padding: 0; | |||
} | |||
} | |||
} |