From 5636c94fe911f670a0f51d63c915c722daf24df5 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 6 Jan 2020 18:22:17 +0100 Subject: [Glitch] Fix reuse of detailed status components Port 12a9813a0cc269d7b7c72095e279f70ddb7bec6e to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/features/status/index.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 322f92477..411d2a88d 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -568,6 +568,7 @@ class Status extends ImmutablePureComponent {
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 ++ .../flavours/glitch/features/local_settings/page/index.js | 8 ++++++++ app/javascript/flavours/glitch/reducers/compose.js | 2 +- app/javascript/flavours/glitch/reducers/local_settings.js | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/features') 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); diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js index e08c12c76..0b3428027 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -166,6 +166,14 @@ class LocalSettingsPage extends React.PureComponent { > + + + 0) { let spoiler_text = action.status.get('spoiler_text'); - if (!spoiler_text.match(/^re[: ]/i)) { + if (action.prependCWRe && !spoiler_text.match(/^re[: ]/i)) { spoiler_text = 're: '.concat(spoiler_text); } map.set('spoiler', true); diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index ad94ea243..3d94d665c 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -17,6 +17,7 @@ const initialState = ImmutableMap({ confirm_missing_media_description: false, confirm_boost_missing_media_description: false, confirm_before_clearing_draft: true, + prepend_cw_re: true, preselect_on_reply: true, inline_preview_cards: true, hicolor_privacy_icons: false, -- cgit From de76a8969ecb353bd31498c03d9441289b796b54 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 22 Jan 2020 16:26:47 +0100 Subject: [Glitch] Fix unfollow confirmations in account directory Port ec3ee675640fa441b9113d69b743ee22e107002b to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/features/directory/components/account_card.js | 1 + 1 file changed, 1 insertion(+) (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.js b/app/javascript/flavours/glitch/features/directory/components/account_card.js index d1c406933..557120960 100644 --- a/app/javascript/flavours/glitch/features/directory/components/account_card.js +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.js @@ -22,6 +22,7 @@ const messages = defineMessages({ requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, }); const makeMapStateToProps = () => { -- cgit From 4f51fe03c904f68f27fe97b78900bb1b1909b677 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 23 Jan 2020 21:32:00 +0100 Subject: [Glitch] Add “account timeline” filter category MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port JS changes from 43daeccccb31a0145c48f41c4ba449f0a3967ef2 to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/features/account_timeline/index.js | 1 + app/javascript/flavours/glitch/selectors/index.js | 1 + 2 files changed, 2 insertions(+) (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 2ef4ff602..f25c82a00 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -112,6 +112,7 @@ class AccountTimeline extends ImmutablePureComponent { onLoadMore={this.handleLoadMore} emptyMessage={} bindToDocument={!multiColumn} + timelineId='account' /> ); diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js index 8ceb71d03..ab7dac66a 100644 --- a/app/javascript/flavours/glitch/selectors/index.js +++ b/app/javascript/flavours/glitch/selectors/index.js @@ -27,6 +27,7 @@ export const toServerSideType = columnType => { case 'notifications': case 'public': case 'thread': + case 'account': return columnType; default: if (columnType.indexOf('list:') > -1) { -- 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/features') 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 1f8563c256a05439f47780857fc0cbc736c30670 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 25 Jan 2020 10:17:13 +0100 Subject: Rename DropdownMenu's ariaLabel to title, to reduce unnecessary changes with upstream --- app/javascript/flavours/glitch/components/dropdown_menu.js | 8 ++++---- .../flavours/glitch/features/status/components/action_bar.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index ab5b7a572..cc4d714e8 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -184,7 +184,7 @@ export default class Dropdown extends React.PureComponent { icon: PropTypes.string.isRequired, items: PropTypes.array.isRequired, size: PropTypes.number.isRequired, - ariaLabel: PropTypes.string, + title: PropTypes.string, disabled: PropTypes.bool, status: ImmutablePropTypes.map, isUserTouching: PropTypes.func, @@ -197,7 +197,7 @@ export default class Dropdown extends React.PureComponent { }; static defaultProps = { - ariaLabel: 'Menu', + title: 'Menu', }; state = { @@ -277,14 +277,14 @@ export default class Dropdown extends React.PureComponent { } render () { - const { icon, items, size, ariaLabel, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props; + const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props; const open = this.state.id === openDropdownId; return (
- +
); -- cgit From 2bde217ea5faf574098fa705839ea0fe7887e6d7 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin Date: Fri, 24 Jan 2020 16:37:04 -0500 Subject: [Glitch] Fix unlocalized dropdown button title Port 1268e3b5726b0650afda20410a714995aa9aeae7 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/features/status/components/action_bar.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 8f6f358d8..d71a3ae08 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -18,6 +18,7 @@ const messages = defineMessages({ cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, + more: { id: 'status.more', defaultMessage: 'More' }, mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, @@ -207,7 +208,7 @@ class ActionBar extends React.PureComponent {
- +
); -- cgit From f1e4738f8111a11005b1f00affb6678c16059ee5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 25 Jan 2020 05:23:05 +0100 Subject: [Glitch] Add number animations Port 76f1ed834efd3b58b6ebc8e951b661bbc1b7bf9b to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/components/animated_number.js | 47 ++++++++++++++++++++++ .../getting_started/components/announcements.js | 5 ++- .../features/status/components/detailed_status.js | 11 ++--- .../glitch/styles/components/announcements.scss | 4 +- .../flavours/glitch/styles/components/index.scss | 8 ++++ 5 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 app/javascript/flavours/glitch/components/animated_number.js (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/components/animated_number.js b/app/javascript/flavours/glitch/components/animated_number.js new file mode 100644 index 000000000..4d14d2924 --- /dev/null +++ b/app/javascript/flavours/glitch/components/animated_number.js @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedNumber } from 'react-intl'; +import TransitionMotion from 'react-motion/lib/TransitionMotion'; +import spring from 'react-motion/lib/spring'; +import { reduceMotion } from 'flavours/glitch/util/initial_state'; + +export default class AnimatedNumber extends React.PureComponent { + + static propTypes = { + value: PropTypes.number.isRequired, + }; + + willEnter () { + return { y: -1 }; + } + + willLeave () { + return { y: spring(1, { damping: 35, stiffness: 400 }) }; + } + + render () { + const { value } = this.props; + + if (reduceMotion) { + return ; + } + + const styles = [{ + key: value, + style: { y: spring(0, { damping: 35, stiffness: 400 }) }, + }]; + + return ( + + {items => ( + + {items.map(({ key, 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 010778727..52c95441b 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js @@ -5,13 +5,14 @@ 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 { defineMessages, injectIntl, FormattedMessage, FormattedDate } 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'; +import AnimatedNumber from 'flavours/glitch/components/animated_number'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -225,7 +226,7 @@ class Reaction extends ImmutablePureComponent { return ( ); } diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 898011c88..c4ac8f0a6 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -7,7 +7,7 @@ import StatusContent from 'flavours/glitch/components/status_content'; import MediaGallery from 'flavours/glitch/components/media_gallery'; import AttachmentList from 'flavours/glitch/components/attachment_list'; import { Link } from 'react-router-dom'; -import { FormattedDate, FormattedNumber } from 'react-intl'; +import { FormattedDate } from 'react-intl'; import Card from './card'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Video from 'flavours/glitch/features/video'; @@ -17,6 +17,7 @@ import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task'; import classNames from 'classnames'; import PollContainer from 'flavours/glitch/containers/poll_container'; import Icon from 'flavours/glitch/components/icon'; +import AnimatedNumber from 'flavours/glitch/components/animated_number'; export default class DetailedStatus extends ImmutablePureComponent { @@ -204,7 +205,7 @@ export default class DetailedStatus extends ImmutablePureComponent { - + ); @@ -213,7 +214,7 @@ export default class DetailedStatus extends ImmutablePureComponent { - + ); @@ -224,7 +225,7 @@ export default class DetailedStatus extends ImmutablePureComponent { - + ); @@ -233,7 +234,7 @@ export default class DetailedStatus extends ImmutablePureComponent { - + ); diff --git a/app/javascript/flavours/glitch/styles/components/announcements.scss b/app/javascript/flavours/glitch/styles/components/announcements.scss index 2e559b1d7..11d29931d 100644 --- a/app/javascript/flavours/glitch/styles/components/announcements.scss +++ b/app/javascript/flavours/glitch/styles/components/announcements.scss @@ -168,10 +168,10 @@ &.active { transition: all 100ms ease-in; transition-property: background-color, color; - background-color: mix(lighten($ui-base-color, 12%), $ui-highlight-color, 90%); + background-color: mix(lighten($ui-base-color, 12%), $ui-highlight-color, 80%); .reactions-bar__item__count { - color: $highlight-text-color; + color: lighten($highlight-text-color, 8%); } } } diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index abe933860..d97ab436d 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -3,6 +3,14 @@ -ms-overflow-style: -ms-autohiding-scrollbar; } +.animated-number { + display: inline-flex; + flex-direction: column; + align-items: stretch; + overflow: hidden; + position: relative; +} + .link-button { display: block; font-size: 15px; -- cgit From 4e1efacfac29cbd4ba3ff675abaa923bb53fcbc8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 25 Jan 2020 05:23:33 +0100 Subject: [Glitch] Add limit of 8 different reaction types per announcement Port f816da9c6474ddf437681fbf7f5346672436ccdf to glitch-soc Signed-off-by: Thibaut Girka --- .../glitch/features/getting_started/components/announcements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/features') 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 52c95441b..71b54b060 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js @@ -265,7 +265,7 @@ class ReactionsBar extends ImmutablePureComponent { /> ))} - } /> + {visibleReactions.size < 8 && } />}
); } -- 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/features') 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 5e4d1f699e9f2ea6954a6d2647412f98ed2ca203 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 25 Jan 2020 19:40:36 +0100 Subject: [Glitch] Fix “new items glow” being displayed above settings and announcements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port 90b13ffd009a431dbe98c37bc92ee59a6f0535f2 to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/components/column_header.js | 5 ++++- .../flavours/glitch/features/home_timeline/index.js | 3 +-- .../flavours/glitch/styles/components/columns.scss | 14 +++++++++++--- 3 files changed, 16 insertions(+), 6 deletions(-) (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js index dd1162429..be45c9535 100644 --- a/app/javascript/flavours/glitch/components/column_header.js +++ b/app/javascript/flavours/glitch/components/column_header.js @@ -42,6 +42,7 @@ class ColumnHeader extends React.PureComponent { onMove: PropTypes.func, onClick: PropTypes.func, intl: PropTypes.object.isRequired, + appendContent: PropTypes.node, }; state = { @@ -106,7 +107,7 @@ class ColumnHeader extends React.PureComponent { } render () { - const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive, placeholder } = this.props; + const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive, placeholder, appendContent } = this.props; const { collapsed, animating, animatingNCD } = this.state; let title = this.props.title; @@ -229,6 +230,8 @@ class ColumnHeader extends React.PureComponent { {(!collapsed || animating) && collapsedContent}
+ + {appendContent} ); diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js index 457ac051c..5e36e5f76 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.js +++ b/app/javascript/flavours/glitch/features/home_timeline/index.js @@ -142,12 +142,11 @@ class HomeTimeline extends React.PureComponent { pinned={pinned} multiColumn={multiColumn} extraButton={announcementsButton} + appendContent={hasAnnouncements && showAnnouncements && } > - {hasAnnouncements && showAnnouncements && } - Date: Sun, 26 Jan 2020 12:17:20 +0100 Subject: Refactor notifications cleaning mode --- .../flavours/glitch/components/column_header.js | 57 ++-------------------- .../glitch/features/notifications/index.js | 55 +++++++++++++++++++-- 2 files changed, 55 insertions(+), 57 deletions(-) (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js index be45c9535..01bd4a246 100644 --- a/app/javascript/flavours/glitch/components/column_header.js +++ b/app/javascript/flavours/glitch/components/column_header.js @@ -2,18 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { createPortal } from 'react-dom'; import classNames from 'classnames'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import Icon from 'flavours/glitch/components/icon'; -import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container'; - const messages = defineMessages({ show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' }, moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, - enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, }); export default @injectIntl @@ -28,27 +24,21 @@ class ColumnHeader extends React.PureComponent { title: PropTypes.node, icon: PropTypes.string, active: PropTypes.bool, - localSettings : ImmutablePropTypes.map, multiColumn: PropTypes.bool, extraButton: PropTypes.node, showBackButton: PropTypes.bool, - notifCleaning: PropTypes.bool, // true only for the notification column - notifCleaningActive: PropTypes.bool, - onEnterCleaningMode: PropTypes.func, children: PropTypes.node, pinned: PropTypes.bool, placeholder: PropTypes.bool, onPin: PropTypes.func, onMove: PropTypes.func, onClick: PropTypes.func, - intl: PropTypes.object.isRequired, appendContent: PropTypes.node, }; state = { collapsed: true, animating: false, - animatingNCD: false, }; historyBack = (skip) => { @@ -90,10 +80,6 @@ class ColumnHeader extends React.PureComponent { this.setState({ animating: false }); } - handleTransitionEndNCD = () => { - this.setState({ animatingNCD: false }); - } - handlePin = () => { if (!this.props.pinned) { this.historyBack(); @@ -101,16 +87,9 @@ class ColumnHeader extends React.PureComponent { this.props.onPin(); } - onEnterCleaningMode = () => { - this.setState({ animatingNCD: true }); - this.props.onEnterCleaningMode(!this.props.notifCleaningActive); - } - render () { - const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive, placeholder, appendContent } = this.props; - const { collapsed, animating, animatingNCD } = this.state; - - let title = this.props.title; + const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent } = this.props; + const { collapsed, animating } = this.state; const wrapperClassName = classNames('column-header__wrapper', { 'active': active, @@ -129,20 +108,8 @@ class ColumnHeader extends React.PureComponent { 'active': !collapsed, }); - const notifCleaningButtonClassName = classNames('column-header__button', { - 'active': notifCleaningActive, - }); - - const notifCleaningDrawerClassName = classNames('ncd column-header__collapsible', { - 'collapsed': !notifCleaningActive, - 'animating': animatingNCD, - }); - let extraContent, pinButton, moveButtons, backButton, collapseButton; - //*glitch - const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning); - if (children) { extraContent = (
@@ -203,28 +170,10 @@ class ColumnHeader extends React.PureComponent {
{hasTitle && backButton} {extraButton} - { notifCleaning ? ( - - ) : null} {collapseButton}
- { notifCleaning ? ( -
-
- {(notifCleaningActive || animatingNCD) ? () : null } -
-
- ) : null} -
{(!collapsed || animating) && collapsedContent} diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index 7f06d70c5..26710feff 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -1,5 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; +import classNames from 'classnames'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from 'flavours/glitch/components/column'; @@ -22,9 +23,13 @@ import { List as ImmutableList } from 'immutable'; import { debounce } from 'lodash'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; import LoadGap from 'flavours/glitch/components/load_gap'; +import Icon from 'flavours/glitch/components/icon'; + +import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container'; const messages = defineMessages({ title: { id: 'column.notifications', defaultMessage: 'Notifications' }, + enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, }); const getNotifications = createSelector([ @@ -94,6 +99,10 @@ class Notifications extends React.PureComponent { trackScroll: true, }; + state = { + animatingNCD: false, + }; + handleLoadGap = (maxId) => { this.props.dispatch(expandNotifications({ maxId })); }; @@ -176,8 +185,19 @@ class Notifications extends React.PureComponent { } } + handleTransitionEndNCD = () => { + this.setState({ animatingNCD: false }); + } + + onEnterCleaningMode = () => { + this.setState({ animatingNCD: true }); + this.props.onEnterCleaningMode(!this.props.notifCleaningActive); + } + render () { const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props; + const { notifCleaning, notifCleaningActive } = this.props; + const { animatingNCD } = this.state; const pinned = !!columnId; const emptyMessage = ; @@ -232,6 +252,36 @@ class Notifications extends React.PureComponent { ); + const notifCleaningButtonClassName = classNames('column-header__button', { + 'active': notifCleaningActive, + }); + + const notifCleaningDrawerClassName = classNames('ncd column-header__collapsible', { + 'collapsed': !notifCleaningActive, + 'animating': animatingNCD, + }); + + const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning); + + const notifCleaningButton = ( + + ); + + const notifCleaningDrawer = ( +
+
+ {(notifCleaningActive || animatingNCD) ? () : null } +
+
+ ); + return ( -- 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/features') 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 b0139dcf5e4ba3fa0e3ba10c40dbe7931fdb0824 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 27 Jan 2020 11:03:45 +0100 Subject: [Glitch] Add animations to announcement reactions Port dd4eec6bf647a082b9bac2bed09a0105fe14c733 to glitch-soc Signed-off-by: Thibaut Girka --- .../getting_started/components/announcements.js | 54 +++++++++++++++------- 1 file changed, 38 insertions(+), 16 deletions(-) (limited to 'app/javascript/flavours/glitch/features') 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 489f53f0f..b56d56606 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js @@ -6,13 +6,15 @@ 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 } from 'react-intl'; -import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, reduceMotion } 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'; import AnimatedNumber from 'flavours/glitch/components/animated_number'; +import TransitionMotion from 'react-motion/lib/TransitionMotion'; +import spring from 'react-motion/lib/spring'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -194,6 +196,7 @@ class Reaction extends ImmutablePureComponent { addReaction: PropTypes.func.isRequired, removeReaction: PropTypes.func.isRequired, emojiMap: ImmutablePropTypes.map.isRequired, + style: PropTypes.object, }; state = { @@ -224,7 +227,7 @@ class Reaction extends ImmutablePureComponent { } return ( - @@ -248,25 +251,44 @@ class ReactionsBar extends ImmutablePureComponent { addReaction(announcementId, data.native.replace(/:/g, '')); } + willEnter () { + return { scale: reduceMotion ? 1 : 0 }; + } + + willLeave () { + return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) }; + } + render () { const { reactions } = this.props; const visibleReactions = reactions.filter(x => x.get('count') > 0); + const styles = visibleReactions.map(reaction => ({ + key: reaction.get('name'), + data: reaction, + style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) }, + })).toArray(); + return ( -
- {visibleReactions.map(reaction => ( - - ))} - - {visibleReactions.size < 8 && } />} -
+ + {items => ( +
+ {items.map(({ key, data, style }) => ( + + ))} + + {visibleReactions.size < 8 && } />} +
+ )} +
); } -- cgit From ed5fb51168efee6e710a899e53e6928b35fa8a49 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 28 Jan 2020 02:20:47 +0100 Subject: [Glitch] Change announcements to be collapsed on page load in web UI Port 0fcc4b1c566e4365e07ce5a1c87743db76c07fe6 to glitch-soc Signed-off-by: Thibaut Girka --- .../glitch/features/getting_started/components/announcements.js | 2 +- app/javascript/flavours/glitch/reducers/announcements.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'app/javascript/flavours/glitch/features') 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 b56d56606..4eeac8560 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js @@ -376,7 +376,7 @@ class Announcements extends ImmutablePureComponent {
- + {announcements.map(announcement => ( 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/features') 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 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/features') 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