From 180f1383943ad171d8394ef9af7c7861bfc08056 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 7 Jan 2020 18:11:50 +0100 Subject: Make prepending β€œre: ” to CWs on reply optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/flavours/glitch/actions/compose.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app/javascript/flavours/glitch/actions') diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index f80642bd8..0be746048 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -91,9 +91,11 @@ export function cycleElefriendCompose() { export function replyCompose(status, routerHistory) { return (dispatch, getState) => { + const prependCWRe = getState().getIn(['local_settings', 'prepend_cw_re']); dispatch({ type: COMPOSE_REPLY, status: status, + prependCWRe: prependCWRe, }); ensureComposeIsVisible(getState, routerHistory); -- cgit From 376e524278a9211777244615b08d5879831cbfe4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 23 Jan 2020 22:00:13 +0100 Subject: [Glitch] Add announcements Port front-end changes from f52c988e12e464e7baefc2fdb48ddf4a95584664 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/actions/announcements.js | 133 +++++++ .../flavours/glitch/actions/importer/normalizer.js | 10 +- .../flavours/glitch/actions/notifications.js | 3 +- .../flavours/glitch/actions/streaming.js | 11 +- .../flavours/glitch/actions/timelines.js | 2 +- .../flavours/glitch/features/emoji_picker/index.js | 7 +- .../getting_started/components/announcements.js | 395 +++++++++++++++++++++ .../containers/announcements_container.js | 21 ++ .../getting_started/containers/trends_container.js | 2 +- .../glitch/features/home_timeline/index.js | 3 + .../glitch/features/ui/components/media_modal.js | 1 - .../flavours/glitch/reducers/announcements.js | 72 ++++ app/javascript/flavours/glitch/reducers/index.js | 2 + .../glitch/styles/components/announcements.scss | 212 +++++++++++ .../flavours/glitch/styles/components/index.scss | 1 + app/javascript/flavours/glitch/styles/forms.scss | 6 + 16 files changed, 872 insertions(+), 9 deletions(-) create mode 100644 app/javascript/flavours/glitch/actions/announcements.js create mode 100644 app/javascript/flavours/glitch/features/getting_started/components/announcements.js create mode 100644 app/javascript/flavours/glitch/features/getting_started/containers/announcements_container.js create mode 100644 app/javascript/flavours/glitch/reducers/announcements.js create mode 100644 app/javascript/flavours/glitch/styles/components/announcements.scss (limited to 'app/javascript/flavours/glitch/actions') diff --git a/app/javascript/flavours/glitch/actions/announcements.js b/app/javascript/flavours/glitch/actions/announcements.js new file mode 100644 index 000000000..d0e5ee176 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/announcements.js @@ -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, +}); diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 2bc603930..52ad17779 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -74,7 +74,6 @@ export function normalizeStatus(status, normalOldStatus) { export function normalizePoll(poll) { const normalPoll = { ...poll }; - const emojiMap = makeEmojiMap(normalPoll); normalPoll.options = poll.options.map((option, index) => ({ @@ -85,3 +84,12 @@ export function normalizePoll(poll) { return normalPoll; } + +export function normalizeAnnouncement(announcement) { + const normalAnnouncement = { ...announcement }; + const emojiMap = makeEmojiMap(normalAnnouncement); + + normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap); + + return normalAnnouncement; +} diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 940f3c3d4..b3de7b5bf 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -168,9 +168,9 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); - done(); }).catch(error => { dispatch(expandNotificationsFail(error, isLoadingMore)); + }).finally(() => { done(); }); }; @@ -199,6 +199,7 @@ export function expandNotificationsFail(error, isLoadingMore) { type: NOTIFICATIONS_EXPAND_FAIL, error, skipLoading: !isLoadingMore, + skipAlert: !isLoadingMore, }; }; diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index 21379f492..8294fbf36 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -8,6 +8,7 @@ import { } from './timelines'; import { updateNotifications, expandNotifications } from './notifications'; import { updateConversations } from './conversations'; +import { fetchAnnouncements, updateAnnouncements, updateReaction as updateAnnouncementsReaction } from './announcements'; import { fetchFilters } from './filters'; import { getLocale } from 'mastodon/locales'; @@ -44,6 +45,12 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null, case 'filters_changed': dispatch(fetchFilters()); break; + case 'announcement': + dispatch(updateAnnouncements(JSON.parse(data.payload))); + break; + case 'announcement.reaction': + dispatch(updateAnnouncementsReaction(JSON.parse(data.payload))); + break; } }, }; @@ -51,7 +58,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null, } const refreshHomeTimelineAndNotification = (dispatch, done) => { - dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done)))); + dispatch(expandHomeTimeline({}, () => + dispatch(expandNotifications({}, () => + dispatch(fetchAnnouncements(done)))))); }; export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index 097878c3b..2ef78025e 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -112,9 +112,9 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { dispatch(importFetchedStatuses(response.data)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); - done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); + }).finally(() => { done(); }); }; diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js index 6e5518b0c..3717fcd82 100644 --- a/app/javascript/flavours/glitch/features/emoji_picker/index.js +++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js @@ -372,6 +372,7 @@ class EmojiPickerDropdown extends React.PureComponent { onPickEmoji: PropTypes.func.isRequired, onSkinTone: PropTypes.func.isRequired, skinTone: PropTypes.number.isRequired, + button: PropTypes.node, }; state = { @@ -432,18 +433,18 @@ class EmojiPickerDropdown extends React.PureComponent { } render () { - const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props; + const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props; const title = intl.formatMessage(messages.emoji); const { active, loading, placement } = this.state; return (
- πŸ™‚ + />}
diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js new file mode 100644 index 000000000..010778727 --- /dev/null +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js @@ -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 ( +
+ ); + } + +} + +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 ( + {emoji} + ); + } else if (emojiMap.get(emoji)) { + const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']); + const shortCode = `:${emoji}:`; + + return ( + {shortCode} + ); + } 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 ( + + ); + } + +} + +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 ( +
+ {visibleReactions.map(reaction => ( + + ))} + + } /> +
+ ); + } + +} + +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 ( +
+ + + {hasTimeRange && Β· - } + + + + + + + +
+ ); + } + +} + +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 ( +
+ + +
+ + {announcements.map(announcement => ( + + ))} + + +
+ + {index + 1} / {announcements.size} + +
+
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/getting_started/containers/announcements_container.js b/app/javascript/flavours/glitch/features/getting_started/containers/announcements_container.js new file mode 100644 index 000000000..b10d1d4ce --- /dev/null +++ b/app/javascript/flavours/glitch/features/getting_started/containers/announcements_container.js @@ -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); diff --git a/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js b/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js index 1df3fb4fe..7a5268780 100644 --- a/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js +++ b/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { fetchTrends } from '../../../actions/trends'; +import { fetchTrends } from 'mastodon/actions/trends'; import Trends from '../components/trends'; const mapStateToProps = state => ({ diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js index 9b71a4404..263371b06 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.js +++ b/app/javascript/flavours/glitch/features/home_timeline/index.js @@ -9,6 +9,7 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnSettingsContainer from './containers/column_settings_container'; import { Link } from 'react-router-dom'; +import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container'; const messages = defineMessages({ title: { id: 'column.home', defaultMessage: 'Home' }, @@ -112,6 +113,8 @@ class HomeTimeline extends React.PureComponent { } + alwaysPrepend trackScroll={!pinned} scrollKey={`home_timeline-${columnId}`} onLoadMore={this.handleLoadMore} diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js index c7d6c374c..23e8dac7e 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js @@ -191,7 +191,6 @@ class MediaModal extends ImmutablePureComponent { style={swipeableViewsStyle} containerStyle={containerStyle} onChangeIndex={this.handleSwipe} - onSwitching={this.handleSwitching} index={index} > {content} diff --git a/app/javascript/flavours/glitch/reducers/announcements.js b/app/javascript/flavours/glitch/reducers/announcements.js new file mode 100644 index 000000000..aa674e516 --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/announcements.js @@ -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; + } +}; diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index 7dbca3a29..586b84749 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/reducers/index.js @@ -35,8 +35,10 @@ import pinnedAccountsEditor from './pinned_accounts_editor'; import polls from './polls'; import identity_proofs from './identity_proofs'; import trends from './trends'; +import announcements from './announcements'; const reducers = { + announcements, dropdown_menu, timelines, meta, diff --git a/app/javascript/flavours/glitch/styles/components/announcements.scss b/app/javascript/flavours/glitch/styles/components/announcements.scss new file mode 100644 index 000000000..0d1f1837b --- /dev/null +++ b/app/javascript/flavours/glitch/styles/components/announcements.scss @@ -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; + } + } +} diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 8e576fd86..abe933860 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -1649,3 +1649,4 @@ noscript { @import 'local_settings'; @import 'error_boundary'; @import 'single_column'; +@import 'announcements'; diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 1920c33ea..396e87c6c 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -213,6 +213,12 @@ code { } } + .input.datetime .label_input select { + display: inline-block; + width: auto; + flex: 0; + } + .required abbr { text-decoration: none; color: lighten($error-value-color, 12%); -- cgit From 6ce72f1fee6ca0752467e4f42ca1bd2dd84b3502 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 25 Jan 2020 16:35:33 +0100 Subject: [Glitch] Improve announcements design Port 48c55b6392661cde8e28cf076c3d132c22d17a0f to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/actions/announcements.js | 18 +++++----- .../getting_started/components/announcements.js | 20 ++--------- .../containers/announcements_container.js | 4 +-- .../glitch/features/home_timeline/index.js | 40 ++++++++++++++++++++-- .../flavours/glitch/reducers/announcements.js | 28 +++++++++++---- .../glitch/styles/components/announcements.scss | 8 +---- 6 files changed, 71 insertions(+), 47 deletions(-) (limited to 'app/javascript/flavours/glitch/actions') diff --git a/app/javascript/flavours/glitch/actions/announcements.js b/app/javascript/flavours/glitch/actions/announcements.js index d0e5ee176..b4e8cee2f 100644 --- a/app/javascript/flavours/glitch/actions/announcements.js +++ b/app/javascript/flavours/glitch/actions/announcements.js @@ -5,7 +5,6 @@ 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'; @@ -17,6 +16,8 @@ export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL = 'ANNOUNCEMENTS_REACTION_REM export const ANNOUNCEMENTS_REACTION_UPDATE = 'ANNOUNCEMENTS_REACTION_UPDATE'; +export const ANNOUNCEMENTS_TOGGLE_SHOW = 'ANNOUNCEMENTS_TOGGLE_SHOW'; + const noOp = () => {}; export const fetchAnnouncements = (done = noOp) => (dispatch, getState) => { @@ -54,15 +55,6 @@ export const updateAnnouncements = announcement => ({ 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)); @@ -131,3 +123,9 @@ export const updateReaction = reaction => ({ type: ANNOUNCEMENTS_REACTION_UPDATE, reaction, }); + +export function toggleShowAnnouncements() { + return dispatch => { + dispatch({ type: ANNOUNCEMENTS_TOGGLE_SHOW }); + }; +} diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js index 71b54b060..9a7d175c4 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js @@ -277,19 +277,13 @@ 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 { announcement } = 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(); @@ -314,8 +308,6 @@ class Announcement extends ImmutablePureComponent { removeReaction={this.props.removeReaction} emojiMap={this.props.emojiMap} /> - -
); } @@ -328,8 +320,6 @@ 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, @@ -339,11 +329,6 @@ class Announcements extends ImmutablePureComponent { index: 0, }; - componentDidMount () { - const { fetchAnnouncements } = this.props; - fetchAnnouncements(); - } - handleChangeIndex = index => { this.setState({ index: index % this.props.announcements.size }); } @@ -369,13 +354,12 @@ class Announcements extends ImmutablePureComponent {
- + {announcements.map(announcement => ( ({ }); 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)), }); diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js index 263371b06..457ac051c 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.js +++ b/app/javascript/flavours/glitch/features/home_timeline/index.js @@ -9,15 +9,23 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnSettingsContainer from './containers/column_settings_container'; import { Link } from 'react-router-dom'; +import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements'; import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container'; +import classNames from 'classnames'; +import IconWithBadge from 'flavours/glitch/components/icon_with_badge'; const messages = defineMessages({ title: { id: 'column.home', defaultMessage: 'Home' }, + show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' }, + hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' }, }); const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, isPartial: state.getIn(['timelines', 'home', 'isPartial']), + hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(), + unreadAnnouncements: state.getIn(['announcements', 'unread']).size, + showAnnouncements: state.getIn(['announcements', 'show']), }); export default @connect(mapStateToProps) @@ -31,6 +39,9 @@ class HomeTimeline extends React.PureComponent { isPartial: PropTypes.bool, columnId: PropTypes.string, multiColumn: PropTypes.bool, + hasAnnouncements: PropTypes.bool, + unreadAnnouncements: PropTypes.number, + showAnnouncements: PropTypes.bool, }; handlePin = () => { @@ -61,6 +72,7 @@ class HomeTimeline extends React.PureComponent { } componentDidMount () { + this.props.dispatch(fetchAnnouncements()); this._checkIfReloadNeeded(false, this.props.isPartial); } @@ -93,10 +105,31 @@ class HomeTimeline extends React.PureComponent { } } + handleToggleAnnouncementsClick = (e) => { + e.stopPropagation(); + this.props.dispatch(toggleShowAnnouncements()); + } + render () { - const { intl, hasUnread, columnId, multiColumn } = this.props; + const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; const pinned = !!columnId; + let announcementsButton = null; + + if (hasAnnouncements) { + announcementsButton = ( + + ); + } + return ( + {hasAnnouncements && showAnnouncements && } + } - alwaysPrepend trackScroll={!pinned} scrollKey={`home_timeline-${columnId}`} onLoadMore={this.handleLoadMore} diff --git a/app/javascript/flavours/glitch/reducers/announcements.js b/app/javascript/flavours/glitch/reducers/announcements.js index aa674e516..1cfb598fb 100644 --- a/app/javascript/flavours/glitch/reducers/announcements.js +++ b/app/javascript/flavours/glitch/reducers/announcements.js @@ -3,18 +3,20 @@ import { 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, + ANNOUNCEMENTS_TOGGLE_SHOW, } from '../actions/announcements'; -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, Set as ImmutableSet, fromJS } from 'immutable'; const initialState = ImmutableMap({ items: ImmutableList(), isLoading: false, + show: true, + unread: ImmutableSet(), }); const updateReaction = (state, id, name, updater) => state.update('items', list => list.map(announcement => { @@ -43,21 +45,35 @@ const addReaction = (state, id, name) => updateReaction(state, id, name, x => x. const removeReaction = (state, id, name) => updateReaction(state, id, name, x => x.set('me', false).update('count', y => y - 1)); +const addUnread = (state, items) => { + if (state.get('show')) return state; + + const newIds = ImmutableSet(items.map(x => x.get('id'))); + const oldIds = ImmutableSet(state.get('items').map(x => x.get('id'))); + return state.update('unread', unread => unread.union(newIds.subtract(oldIds))); +}; + export default function announcementsReducer(state = initialState, action) { switch(action.type) { + case ANNOUNCEMENTS_TOGGLE_SHOW: + return state.withMutations(map => { + if (!map.get('show')) map.set('unread', ImmutableSet()); + map.set('show', !map.get('show')); + }); case ANNOUNCEMENTS_FETCH_REQUEST: return state.set('isLoading', true); case ANNOUNCEMENTS_FETCH_SUCCESS: return state.withMutations(map => { - map.set('items', fromJS(action.announcements)); + const items = fromJS(action.announcements); + map.set('unread', ImmutableSet()); + addUnread(map, items); + map.set('items', items); 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)); + return addUnread(state, [fromJS(action.announcement)]).update('items', list => list.unshift(fromJS(action.announcement)).sortBy(announcement => announcement.get('starts_at'))); case ANNOUNCEMENTS_REACTION_UPDATE: return updateReactionCount(state, action.reaction); case ANNOUNCEMENTS_REACTION_ADD_REQUEST: diff --git a/app/javascript/flavours/glitch/styles/components/announcements.scss b/app/javascript/flavours/glitch/styles/components/announcements.scss index 11d29931d..6bf9e2a0c 100644 --- a/app/javascript/flavours/glitch/styles/components/announcements.scss +++ b/app/javascript/flavours/glitch/styles/components/announcements.scss @@ -37,7 +37,7 @@ } .announcements { - background: lighten($ui-base-color, 4%); + background: lighten($ui-base-color, 8%); border-top: 1px solid $ui-base-color; font-size: 13px; display: flex; @@ -78,12 +78,6 @@ font-weight: 500; margin-bottom: 10px; } - - &__dismiss-icon { - position: absolute; - top: 12px; - right: 12px; - } } &__pagination { -- cgit From 98d5ab6be9a48ab190ca941164d17afdbe3ea46e Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 25 Jan 2020 18:19:24 +0100 Subject: [Glitch] Fix spurious error and incorrect state change when adding a reaction twice Port c06d2ff43718af5940987e219379094f1a2de180 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/actions/announcements.js | 21 ++++++++++++++++++--- .../glitch/styles/components/announcements.scss | 1 - 2 files changed, 18 insertions(+), 4 deletions(-) (limited to 'app/javascript/flavours/glitch/actions') diff --git a/app/javascript/flavours/glitch/actions/announcements.js b/app/javascript/flavours/glitch/actions/announcements.js index b4e8cee2f..930493b6a 100644 --- a/app/javascript/flavours/glitch/actions/announcements.js +++ b/app/javascript/flavours/glitch/actions/announcements.js @@ -56,12 +56,27 @@ export const updateAnnouncements = announcement => ({ }); export const addReaction = (announcementId, name) => (dispatch, getState) => { - dispatch(addReactionRequest(announcementId, name)); + const announcement = getState().getIn(['announcements', 'items']).find(x => x.get('id') === announcementId); + + let alreadyAdded = false; + + if (announcement) { + const reaction = announcement.get('reactions').find(x => x.get('name') === name); + if (reaction && reaction.get('me')) { + alreadyAdded = true; + } + } + + if (!alreadyAdded) { + dispatch(addReactionRequest(announcementId, name, alreadyAdded)); + } api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => { - dispatch(addReactionSuccess(announcementId, name)); + dispatch(addReactionSuccess(announcementId, name, alreadyAdded)); }).catch(err => { - dispatch(addReactionFail(announcementId, name, err)); + if (!alreadyAdded) { + dispatch(addReactionFail(announcementId, name, err)); + } }); }; diff --git a/app/javascript/flavours/glitch/styles/components/announcements.scss b/app/javascript/flavours/glitch/styles/components/announcements.scss index 6bf9e2a0c..00585cb1c 100644 --- a/app/javascript/flavours/glitch/styles/components/announcements.scss +++ b/app/javascript/flavours/glitch/styles/components/announcements.scss @@ -38,7 +38,6 @@ .announcements { background: lighten($ui-base-color, 8%); - border-top: 1px solid $ui-base-color; font-size: 13px; display: flex; align-items: flex-end; -- cgit From e36d0a98cc10575e67c526ef3cc9eeb431a1af3f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 26 Jan 2020 20:07:26 +0100 Subject: [Glitch] Add streaming API updates for announcements being modified or deleted Port b9d74d407673a6dbdc87c3310618b22c85358c85 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/actions/announcements.js | 14 +++--- .../flavours/glitch/actions/streaming.js | 10 ++++- .../flavours/glitch/components/animated_number.js | 7 +-- .../getting_started/components/announcements.js | 12 ++--- .../flavours/glitch/reducers/announcements.js | 51 +++++++++++++++++----- 5 files changed, 69 insertions(+), 25 deletions(-) (limited to 'app/javascript/flavours/glitch/actions') diff --git a/app/javascript/flavours/glitch/actions/announcements.js b/app/javascript/flavours/glitch/actions/announcements.js index 930493b6a..a19a4c708 100644 --- a/app/javascript/flavours/glitch/actions/announcements.js +++ b/app/javascript/flavours/glitch/actions/announcements.js @@ -5,6 +5,7 @@ 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_DELETE = 'ANNOUNCEMENTS_DELETE'; export const ANNOUNCEMENTS_REACTION_ADD_REQUEST = 'ANNOUNCEMENTS_REACTION_ADD_REQUEST'; export const ANNOUNCEMENTS_REACTION_ADD_SUCCESS = 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS'; @@ -139,8 +140,11 @@ export const updateReaction = reaction => ({ reaction, }); -export function toggleShowAnnouncements() { - return dispatch => { - dispatch({ type: ANNOUNCEMENTS_TOGGLE_SHOW }); - }; -} +export const toggleShowAnnouncements = () => ({ + type: ANNOUNCEMENTS_TOGGLE_SHOW, +}); + +export const deleteAnnouncement = id => ({ + type: ANNOUNCEMENTS_DELETE, + id, +}); diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index 8294fbf36..2f82ea805 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -8,7 +8,12 @@ import { } from './timelines'; import { updateNotifications, expandNotifications } from './notifications'; import { updateConversations } from './conversations'; -import { fetchAnnouncements, updateAnnouncements, updateReaction as updateAnnouncementsReaction } from './announcements'; +import { + fetchAnnouncements, + updateAnnouncements, + updateReaction as updateAnnouncementsReaction, + deleteAnnouncement, +} from './announcements'; import { fetchFilters } from './filters'; import { getLocale } from 'mastodon/locales'; @@ -51,6 +56,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null, case 'announcement.reaction': dispatch(updateAnnouncementsReaction(JSON.parse(data.payload))); break; + case 'announcement.delete': + dispatch(deleteAnnouncement(data.payload)); + break; } }, }; diff --git a/app/javascript/flavours/glitch/components/animated_number.js b/app/javascript/flavours/glitch/components/animated_number.js index 4d14d2924..6f3d30d4e 100644 --- a/app/javascript/flavours/glitch/components/animated_number.js +++ b/app/javascript/flavours/glitch/components/animated_number.js @@ -27,7 +27,8 @@ export default class AnimatedNumber extends React.PureComponent { } const styles = [{ - key: value, + key: `${value}`, + data: value, style: { y: spring(0, { damping: 35, stiffness: 400 }) }, }]; @@ -35,8 +36,8 @@ export default class AnimatedNumber extends React.PureComponent { {items => ( - {items.map(({ key, style }) => ( - 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}> + {items.map(({ key, data, style }) => ( + 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}> ))} )} diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js index 9a7d175c4..489f53f0f 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js @@ -367,11 +367,13 @@ class Announcements extends ImmutablePureComponent { ))} -
- - {index + 1} / {announcements.size} - -
+ {announcements.size > 1 && ( +
+ + {index + 1} / {announcements.size} + +
+ )}
); diff --git a/app/javascript/flavours/glitch/reducers/announcements.js b/app/javascript/flavours/glitch/reducers/announcements.js index 1cfb598fb..3215c1c2d 100644 --- a/app/javascript/flavours/glitch/reducers/announcements.js +++ b/app/javascript/flavours/glitch/reducers/announcements.js @@ -9,6 +9,7 @@ import { ANNOUNCEMENTS_REACTION_REMOVE_REQUEST, ANNOUNCEMENTS_REACTION_REMOVE_FAIL, ANNOUNCEMENTS_TOGGLE_SHOW, + ANNOUNCEMENTS_DELETE, } from '../actions/announcements'; import { Map as ImmutableMap, List as ImmutableList, Set as ImmutableSet, fromJS } from 'immutable'; @@ -22,14 +23,10 @@ const initialState = ImmutableMap({ 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; - }); + const idx = reactions.findIndex(reaction => reaction.get('name') === name); + + if (idx > -1) { + return reactions.update(idx, reaction => updater(reaction)); } return reactions.push(updater(fromJS({ name, count: 0 }))); @@ -46,13 +43,33 @@ const addReaction = (state, id, name) => updateReaction(state, id, name, x => x. const removeReaction = (state, id, name) => updateReaction(state, id, name, x => x.set('me', false).update('count', y => y - 1)); const addUnread = (state, items) => { - if (state.get('show')) return state; + if (state.get('show')) { + return state; + } const newIds = ImmutableSet(items.map(x => x.get('id'))); const oldIds = ImmutableSet(state.get('items').map(x => x.get('id'))); + return state.update('unread', unread => unread.union(newIds.subtract(oldIds))); }; +const sortAnnouncements = list => list.sortBy(x => x.get('starts_at') || x.get('published_at')); + +const updateAnnouncement = (state, announcement) => { + const idx = state.get('items').findIndex(x => x.get('id') === announcement.get('id')); + + state = addUnread(state, [announcement]); + + if (idx > -1) { + // Deep merge is used because announcements from the streaming API do not contain + // personalized data about which reactions have been selected by the given user, + // and that is information we want to preserve + return state.update('items', list => sortAnnouncements(list.update(idx, x => x.mergeDeep(announcement)))); + } + + return state.update('items', list => sortAnnouncements(list.unshift(announcement))); +}; + export default function announcementsReducer(state = initialState, action) { switch(action.type) { case ANNOUNCEMENTS_TOGGLE_SHOW: @@ -65,15 +82,17 @@ export default function announcementsReducer(state = initialState, action) { case ANNOUNCEMENTS_FETCH_SUCCESS: return state.withMutations(map => { const items = fromJS(action.announcements); + map.set('unread', ImmutableSet()); - addUnread(map, items); map.set('items', items); map.set('isLoading', false); + + addUnread(map, items); }); case ANNOUNCEMENTS_FETCH_FAIL: return state.set('isLoading', false); case ANNOUNCEMENTS_UPDATE: - return addUnread(state, [fromJS(action.announcement)]).update('items', list => list.unshift(fromJS(action.announcement)).sortBy(announcement => announcement.get('starts_at'))); + return updateAnnouncement(state, fromJS(action.announcement)); case ANNOUNCEMENTS_REACTION_UPDATE: return updateReactionCount(state, action.reaction); case ANNOUNCEMENTS_REACTION_ADD_REQUEST: @@ -82,6 +101,16 @@ export default function announcementsReducer(state = initialState, action) { case ANNOUNCEMENTS_REACTION_REMOVE_REQUEST: case ANNOUNCEMENTS_REACTION_ADD_FAIL: return removeReaction(state, action.id, action.name); + case ANNOUNCEMENTS_DELETE: + return state.update('unread', set => set.delete(action.id)).update('items', list => { + const idx = list.findIndex(x => x.get('id') === action.id); + + if (idx > -1) { + return list.delete(idx); + } + + return list; + }); default: return state; } -- cgit From e135b293fae2a9f83975ab0ce4b66c65f90a8f56 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 1 Feb 2020 17:03:39 +0100 Subject: Fetch last read notification id to update unread notification count on load --- app/javascript/flavours/glitch/actions/markers.js | 42 ++++++++++++++++++++++ .../flavours/glitch/features/ui/index.js | 3 +- .../flavours/glitch/reducers/notifications.js | 19 +++++++++- 3 files changed, 62 insertions(+), 2 deletions(-) (limited to 'app/javascript/flavours/glitch/actions') diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index c3a5fe86f..defcb7127 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -1,3 +1,9 @@ +import api from 'flavours/glitch/util/api'; + +export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; +export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; +export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL'; + export const submitMarkers = () => (dispatch, getState) => { const accessToken = getState().getIn(['meta', 'access_token'], ''); const params = {}; @@ -28,3 +34,39 @@ export const submitMarkers = () => (dispatch, getState) => { client.setRequestHeader('Authorization', `Bearer ${accessToken}`); client.send(JSON.stringify(params)); }; + +export const fetchMarkers = () => (dispatch, getState) => { + const params = { timeline: ['notifications'] }; + + dispatch(fetchMarkersRequest()); + + api(getState).get('/api/v1/markers', { params }).then(response => { + dispatch(fetchMarkersSuccess(response.data)); + }).catch(error => { + dispatch(fetchMarkersFail(error)); + }); +}; + +export function fetchMarkersRequest() { + return { + type: MARKERS_FETCH_REQUEST, + skipLoading: true, + }; +}; + +export function fetchMarkersSuccess(markers) { + return { + type: MARKERS_FETCH_SUCCESS, + markers, + skipLoading: true, + }; +}; + +export function fetchMarkersFail(error) { + return { + type: MARKERS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, + }; +}; diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 5c861fdee..9f9ef561a 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -12,7 +12,7 @@ import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications'; import { fetchFilters } from 'flavours/glitch/actions/filters'; import { clearHeight } from 'flavours/glitch/actions/height_cache'; -import { submitMarkers } from 'flavours/glitch/actions/markers'; +import { submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers'; import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers'; import UploadArea from './components/upload_area'; import PermaLink from 'flavours/glitch/components/permalink'; @@ -388,6 +388,7 @@ class UI extends React.Component { this.favicon = new Favico({ animation:"none" }); + this.props.dispatch(fetchMarkers()); this.props.dispatch(expandHomeTimeline()); this.props.dispatch(expandNotifications()); setTimeout(() => this.props.dispatch(fetchFilters()), 500); diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index 3623e90da..d0eb7bb5a 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -23,6 +23,9 @@ import { FOLLOW_REQUEST_AUTHORIZE_SUCCESS, FOLLOW_REQUEST_REJECT_SUCCESS, } from 'flavours/glitch/actions/accounts'; +import { + MARKERS_FETCH_SUCCESS, +} from 'flavours/glitch/actions/markers'; import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks'; import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; @@ -104,7 +107,7 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece mutable.update('lastReadId', id => compareId(id, items.first().get('id')) > 0 ? id : items.first().get('id')); } } else { - mutable.update('unread', unread => unread + items.filter(item => compareId(item.get('id'), lastReadId) > 0).size); + mutable.update('unread', unread => unread + items.count(item => compareId(item.get('id'), lastReadId) > 0)); } if (!next) { @@ -197,10 +200,24 @@ const shouldCountUnreadNotifications = (state) => { return !(state.get('isTabVisible') && state.get('top') && state.get('mounted') > 0); }; +const recountUnread = (state, last_read_id) => { + return state.withMutations(mutable => { + if (compareId(last_read_id, mutable.get('lastReadId')) > 0) { + mutable.set('lastReadId', last_read_id); + } + + if (state.get('unread') > 0 || shouldCountUnreadNotifications(state)) { + mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), last_read_id) > 0)); + } + }); +} + export default function notifications(state = initialState, action) { let st; switch(action.type) { + case MARKERS_FETCH_SUCCESS: + return action.markers.notifications ? recountUnread(state, action.markers.notifications.last_read_id) : state; case NOTIFICATIONS_MOUNT: return updateMounted(state); case NOTIFICATIONS_UNMOUNT: -- cgit From 4cd2d13bd226c005317adf9848f4c0316401b2ff Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 1 Feb 2020 17:53:28 +0100 Subject: When submitting markers, use last displayed notification, not last received one --- app/javascript/flavours/glitch/actions/markers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/javascript/flavours/glitch/actions') diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index defcb7127..7ffab404d 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -9,7 +9,7 @@ export const submitMarkers = () => (dispatch, getState) => { const params = {}; const lastHomeId = getState().getIn(['timelines', 'home', 'items', 0]); - const lastNotificationId = getState().getIn(['notifications', 'items', 0, 'id']); + const lastNotificationId = getState().getIn(['notifications', 'lastReadId']); if (lastHomeId) { params.home = { @@ -17,7 +17,7 @@ export const submitMarkers = () => (dispatch, getState) => { }; } - if (lastNotificationId) { + if (lastNotificationId && lastNotificationId !== '0') { params.notifications = { last_read_id: lastNotificationId, }; -- cgit From 3dcb279da31bc7810b7f58991dad4f886aaade34 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 3 Feb 2020 01:53:09 +0100 Subject: [Glitch] Change how unread announcements are handled Port 3adc722d1cdd28d87d2724b8952d7ec52d241b52 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/actions/announcements.js | 30 +++++++++++++++++++ .../getting_started/components/announcements.js | 34 +++++++++++++++++++++- .../containers/announcements_container.js | 3 +- .../glitch/features/home_timeline/index.js | 2 +- .../flavours/glitch/reducers/announcements.js | 26 ++++------------- .../glitch/styles/components/announcements.scss | 12 ++++++++ 6 files changed, 83 insertions(+), 24 deletions(-) (limited to 'app/javascript/flavours/glitch/actions') diff --git a/app/javascript/flavours/glitch/actions/announcements.js b/app/javascript/flavours/glitch/actions/announcements.js index a19a4c708..871409d43 100644 --- a/app/javascript/flavours/glitch/actions/announcements.js +++ b/app/javascript/flavours/glitch/actions/announcements.js @@ -7,6 +7,10 @@ export const ANNOUNCEMENTS_FETCH_FAIL = 'ANNOUNCEMENTS_FETCH_FAIL'; export const ANNOUNCEMENTS_UPDATE = 'ANNOUNCEMENTS_UPDATE'; export const ANNOUNCEMENTS_DELETE = 'ANNOUNCEMENTS_DELETE'; +export const ANNOUNCEMENTS_DISMISS_REQUEST = 'ANNOUNCEMENTS_DISMISS_REQUEST'; +export const ANNOUNCEMENTS_DISMISS_SUCCESS = 'ANNOUNCEMENTS_DISMISS_SUCCESS'; +export const ANNOUNCEMENTS_DISMISS_FAIL = 'ANNOUNCEMENTS_DISMISS_FAIL'; + 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'; @@ -56,6 +60,32 @@ export const updateAnnouncements = announcement => ({ announcement: normalizeAnnouncement(announcement), }); +export const dismissAnnouncement = announcementId => (dispatch, getState) => { + dispatch(dismissAnnouncementRequest(announcementId)); + + api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => { + dispatch(dismissAnnouncementSuccess(announcementId)); + }).catch(error => { + dispatch(dismissAnnouncementFail(announcementId, error)); + }); +}; + +export const dismissAnnouncementRequest = announcementId => ({ + type: ANNOUNCEMENTS_DISMISS_REQUEST, + id: announcementId, +}); + +export const dismissAnnouncementSuccess = announcementId => ({ + type: ANNOUNCEMENTS_DISMISS_SUCCESS, + id: announcementId, +}); + +export const dismissAnnouncementFail = (announcementId, error) => ({ + type: ANNOUNCEMENTS_DISMISS_FAIL, + id: announcementId, + error, +}); + export const addReaction = (announcementId, name) => (dispatch, getState) => { const announcement = getState().getIn(['announcements', 'items']).find(x => x.get('id') === announcementId); diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js index 4eeac8560..e34c9009b 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js @@ -302,10 +302,23 @@ class Announcement extends ImmutablePureComponent { addReaction: PropTypes.func.isRequired, removeReaction: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + selected: PropTypes.bool, }; + state = { + unread: !this.props.announcement.get('read'), + }; + + componentDidUpdate () { + const { selected, announcement } = this.props; + if (!selected && this.state.unread !== !announcement.get('read')) { + this.setState({ unread: !announcement.get('read') }); + } + } + render () { const { announcement } = this.props; + const { unread } = this.state; 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(); @@ -330,6 +343,8 @@ class Announcement extends ImmutablePureComponent { removeReaction={this.props.removeReaction} emojiMap={this.props.emojiMap} /> + + {unread && } ); } @@ -342,6 +357,7 @@ class Announcements extends ImmutablePureComponent { static propTypes = { announcements: ImmutablePropTypes.list, emojiMap: ImmutablePropTypes.map.isRequired, + dismissAnnouncement: PropTypes.func.isRequired, addReaction: PropTypes.func.isRequired, removeReaction: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -351,6 +367,21 @@ class Announcements extends ImmutablePureComponent { index: 0, }; + componentDidMount () { + this._markAnnouncementAsRead(); + } + + componentDidUpdate () { + this._markAnnouncementAsRead(); + } + + _markAnnouncementAsRead () { + const { dismissAnnouncement, announcements } = this.props; + const { index } = this.state; + const announcement = announcements.get(index); + if (!announcement.get('read')) dismissAnnouncement(announcement.get('id')); + } + handleChangeIndex = index => { this.setState({ index: index % this.props.announcements.size }); } @@ -377,7 +408,7 @@ class Announcements extends ImmutablePureComponent {
- {announcements.map(announcement => ( + {announcements.map((announcement, idx) => ( ))} diff --git a/app/javascript/flavours/glitch/features/getting_started/containers/announcements_container.js b/app/javascript/flavours/glitch/features/getting_started/containers/announcements_container.js index 8fa695e34..d472323f8 100644 --- a/app/javascript/flavours/glitch/features/getting_started/containers/announcements_container.js +++ b/app/javascript/flavours/glitch/features/getting_started/containers/announcements_container.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { addReaction, removeReaction } from 'flavours/glitch/actions/announcements'; +import { addReaction, removeReaction, dismissAnnouncement } from 'flavours/glitch/actions/announcements'; import Announcements from '../components/announcements'; import { createSelector } from 'reselect'; import { Map as ImmutableMap } from 'immutable'; @@ -12,6 +12,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ + dismissAnnouncement: id => dispatch(dismissAnnouncement(id)), addReaction: (id, name) => dispatch(addReaction(id, name)), removeReaction: (id, name) => dispatch(removeReaction(id, name)), }); diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js index 5e36e5f76..cc8e4664c 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.js +++ b/app/javascript/flavours/glitch/features/home_timeline/index.js @@ -24,7 +24,7 @@ const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, isPartial: state.getIn(['timelines', 'home', 'isPartial']), hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(), - unreadAnnouncements: state.getIn(['announcements', 'unread']).size, + unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')), showAnnouncements: state.getIn(['announcements', 'show']), }); diff --git a/app/javascript/flavours/glitch/reducers/announcements.js b/app/javascript/flavours/glitch/reducers/announcements.js index 1653318ce..34e08eac8 100644 --- a/app/javascript/flavours/glitch/reducers/announcements.js +++ b/app/javascript/flavours/glitch/reducers/announcements.js @@ -10,14 +10,14 @@ import { ANNOUNCEMENTS_REACTION_REMOVE_FAIL, ANNOUNCEMENTS_TOGGLE_SHOW, ANNOUNCEMENTS_DELETE, + ANNOUNCEMENTS_DISMISS_SUCCESS, } from '../actions/announcements'; -import { Map as ImmutableMap, List as ImmutableList, Set as ImmutableSet, fromJS } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const initialState = ImmutableMap({ items: ImmutableList(), isLoading: false, show: false, - unread: ImmutableSet(), }); const updateReaction = (state, id, name, updater) => state.update('items', list => list.map(announcement => { @@ -42,24 +42,11 @@ const addReaction = (state, id, name) => updateReaction(state, id, name, x => x. const removeReaction = (state, id, name) => updateReaction(state, id, name, x => x.set('me', false).update('count', y => y - 1)); -const addUnread = (state, items) => { - if (state.get('show')) { - return state; - } - - const newIds = ImmutableSet(items.map(x => x.get('id'))); - const oldIds = ImmutableSet(state.get('items').map(x => x.get('id'))); - - return state.update('unread', unread => unread.union(newIds.subtract(oldIds))); -}; - const sortAnnouncements = list => list.sortBy(x => x.get('starts_at') || x.get('published_at')); const updateAnnouncement = (state, announcement) => { const idx = state.get('items').findIndex(x => x.get('id') === announcement.get('id')); - state = addUnread(state, [announcement]); - if (idx > -1) { // Deep merge is used because announcements from the streaming API do not contain // personalized data about which reactions have been selected by the given user, @@ -74,7 +61,6 @@ export default function announcementsReducer(state = initialState, action) { switch(action.type) { case ANNOUNCEMENTS_TOGGLE_SHOW: return state.withMutations(map => { - if (!map.get('show')) map.set('unread', ImmutableSet()); map.set('show', !map.get('show')); }); case ANNOUNCEMENTS_FETCH_REQUEST: @@ -83,10 +69,6 @@ export default function announcementsReducer(state = initialState, action) { return state.withMutations(map => { const items = fromJS(action.announcements); - map.set('unread', ImmutableSet()); - - addUnread(map, items); - map.set('items', items); map.set('isLoading', false); }); @@ -102,8 +84,10 @@ export default function announcementsReducer(state = initialState, action) { case ANNOUNCEMENTS_REACTION_REMOVE_REQUEST: case ANNOUNCEMENTS_REACTION_ADD_FAIL: return removeReaction(state, action.id, action.name); + case ANNOUNCEMENTS_DISMISS_SUCCESS: + return updateAnnouncement(state, fromJS({ 'id': action.id, 'read': true })); case ANNOUNCEMENTS_DELETE: - return state.update('unread', set => set.delete(action.id)).update('items', list => { + return state.update('items', list => { const idx = list.findIndex(x => x.get('id') === action.id); if (idx > -1) { diff --git a/app/javascript/flavours/glitch/styles/components/announcements.scss b/app/javascript/flavours/glitch/styles/components/announcements.scss index 909957bf1..ac4c199cd 100644 --- a/app/javascript/flavours/glitch/styles/components/announcements.scss +++ b/app/javascript/flavours/glitch/styles/components/announcements.scss @@ -81,6 +81,18 @@ font-weight: 500; margin-bottom: 10px; } + + &__unread { + position: absolute; + top: 15px; + right: 15px; + display: inline-block; + background: $highlight-text-color; + border-radius: 50%; + width: 0.625rem; + height: 0.625rem; + margin: 0 .15em; + } } &__pagination { -- cgit