From ae698469d0c129216580e222d6d6ca82eab4516b Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 15 Sep 2020 17:58:05 +0200 Subject: Clean up leftover from DM conversation view redesign --- app/javascript/flavours/glitch/components/status.js | 2 +- app/javascript/flavours/glitch/styles/components/status.scss | 6 +++--- app/javascript/flavours/glitch/styles/mastodon-light/diff.scss | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 4e628a420..fe0234d84 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -694,7 +694,7 @@ class Status extends ImmutablePureComponent { collapsed: isCollapsed, 'has-background': isCollapsed && background, 'status__wrapper-reply': !!status.get('in_reply_to_id'), - read: unread === false, + unread, muted, }, 'focusable'); diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index a65581136..f3403a243 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -275,7 +275,7 @@ outline: 0; background: lighten($ui-base-color, 4%); - &.status.status-direct:not(.read) { + &.status.status-direct { background: lighten($ui-base-color, 12%); &.muted { @@ -316,7 +316,7 @@ margin-top: 8px; } - &.status-direct:not(.read) { + &.status-direct { background: lighten($ui-base-color, 8%); border-bottom-color: lighten($ui-base-color, 12%); } @@ -399,7 +399,7 @@ &:focus > .status__content:after { background: linear-gradient(rgba(lighten($ui-base-color, 4%), 0), rgba(lighten($ui-base-color, 4%), 1)); } - &.status-direct:not(.read)> .status__content:after { + &.status-direct > .status__content:after { background: linear-gradient(rgba(lighten($ui-base-color, 8%), 0), rgba(lighten($ui-base-color, 8%), 1)); } diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index c83c82766..e5a5cc246 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -27,7 +27,7 @@ } } -.status.status-direct:not(.read) { +.status.status-direct { background: darken($ui-base-color, 8%); border-bottom-color: darken($ui-base-color, 12%); @@ -36,7 +36,7 @@ } } -.focusable:focus.status.status-direct:not(.read) { +.focusable:focus.status.status-direct { background: darken($ui-base-color, 4%); &.collapsed> .status__content:after { -- cgit From 66e9a77e36a67a5c29feb8be5b1e323b463e0468 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 15 Sep 2020 18:09:08 +0200 Subject: Display unread marker for notifications --- app/javascript/flavours/glitch/components/status.js | 1 + .../features/notifications/components/follow.js | 6 ++++-- .../notifications/components/follow_request.js | 6 ++++-- .../features/notifications/components/notification.js | 6 ++++++ .../flavours/glitch/features/notifications/index.js | 6 +++++- .../flavours/glitch/styles/components/status.scss | 19 +++++++++++++++++++ 6 files changed, 39 insertions(+), 5 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index fe0234d84..8da5db961 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -122,6 +122,7 @@ class Status extends ImmutablePureComponent { 'notification', 'hidden', 'expanded', + 'unread', ] updateOnStates = [ diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow.js b/app/javascript/flavours/glitch/features/notifications/components/follow.js index 2b71f3107..0d3162fc9 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/follow.js +++ b/app/javascript/flavours/glitch/features/notifications/components/follow.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; +import classNames from 'classnames'; // Our imports. import Permalink from 'flavours/glitch/components/permalink'; @@ -19,6 +20,7 @@ export default class NotificationFollow extends ImmutablePureComponent { id: PropTypes.string.isRequired, account: ImmutablePropTypes.map.isRequired, notification: ImmutablePropTypes.map.isRequired, + unread: PropTypes.bool, }; handleMoveUp = () => { @@ -59,7 +61,7 @@ export default class NotificationFollow extends ImmutablePureComponent { } render () { - const { account, notification, hidden } = this.props; + const { account, notification, hidden, unread } = this.props; // Links to the display name. const displayName = account.get('display_name_html') || account.get('username'); @@ -76,7 +78,7 @@ export default class NotificationFollow extends ImmutablePureComponent { // Renders. return ( -
+
diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow_request.js b/app/javascript/flavours/glitch/features/notifications/components/follow_request.js index d73dac434..f351c1035 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/follow_request.js +++ b/app/javascript/flavours/glitch/features/notifications/components/follow_request.js @@ -10,6 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import NotificationOverlayContainer from '../containers/overlay_container'; import { HotKeys } from 'react-hotkeys'; import Icon from 'flavours/glitch/components/icon'; +import classNames from 'classnames'; const messages = defineMessages({ authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' }, @@ -25,6 +26,7 @@ class FollowRequest extends ImmutablePureComponent { onReject: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, notification: ImmutablePropTypes.map.isRequired, + unread: PropTypes.bool, }; handleMoveUp = () => { @@ -65,7 +67,7 @@ class FollowRequest extends ImmutablePureComponent { } render () { - const { intl, hidden, account, onAuthorize, onReject, notification } = this.props; + const { intl, hidden, account, onAuthorize, onReject, notification, unread } = this.props; if (!account) { return
; @@ -94,7 +96,7 @@ class FollowRequest extends ImmutablePureComponent { return ( -
+
diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js index 62fc28386..7b80c2228 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.js +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js @@ -46,6 +46,7 @@ export default class Notification extends ImmutablePureComponent { onMoveDown={onMoveDown} onMoveUp={onMoveUp} onMention={onMention} + unread={this.props.unread} /> ); case 'follow_request': @@ -58,6 +59,7 @@ export default class Notification extends ImmutablePureComponent { onMoveDown={onMoveDown} onMoveUp={onMoveUp} onMention={onMention} + unread={this.props.unread} /> ); case 'mention': @@ -77,6 +79,7 @@ export default class Notification extends ImmutablePureComponent { cacheMediaWidth={this.props.cacheMediaWidth} onUnmount={this.props.onUnmount} withDismiss + unread={this.props.unread} /> ); case 'favourite': @@ -98,6 +101,7 @@ export default class Notification extends ImmutablePureComponent { cacheMediaWidth={this.props.cacheMediaWidth} onUnmount={this.props.onUnmount} withDismiss + unread={this.props.unread} /> ); case 'reblog': @@ -119,6 +123,7 @@ export default class Notification extends ImmutablePureComponent { cacheMediaWidth={this.props.cacheMediaWidth} onUnmount={this.props.onUnmount} withDismiss + unread={this.props.unread} /> ); case 'poll': @@ -140,6 +145,7 @@ export default class Notification extends ImmutablePureComponent { cacheMediaWidth={this.props.cacheMediaWidth} onUnmount={this.props.onUnmount} withDismiss + unread={this.props.unread} /> ); default: diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index 26710feff..cef0a0d2a 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -24,6 +24,7 @@ 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 compareId from 'flavours/glitch/util/compare_id'; import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container'; @@ -56,6 +57,7 @@ const mapStateToProps = state => ({ hasMore: state.getIn(['notifications', 'hasMore']), numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), + lastReadId: state.getIn(['notifications', 'lastReadId']), }); /* glitch */ @@ -93,6 +95,7 @@ class Notifications extends React.PureComponent { onEnterCleaningMode: PropTypes.func, onMount: PropTypes.func, onUnmount: PropTypes.func, + lastReadId: PropTypes.string, }; static defaultProps = { @@ -195,7 +198,7 @@ class Notifications extends React.PureComponent { } render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props; + const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId } = this.props; const { notifCleaning, notifCleaningActive } = this.props; const { animatingNCD } = this.state; const pinned = !!columnId; @@ -224,6 +227,7 @@ class Notifications extends React.PureComponent { accountId={item.get('account')} onMoveUp={this.handleMoveUp} onMoveDown={this.handleMoveDown} + unread={lastReadId && compareId(item.get('id'), lastReadId) > 0} /> )); } else { diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index f3403a243..ba75e3ffe 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -1054,3 +1054,22 @@ a.status-card.compact:hover { text-decoration: underline; } } + +.notification, +.status { + position: relative; + + &.unread { + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + pointer-events: 0; + width: 100%; + height: 100%; + border-left: 2px solid $highlight-text-color; + pointer-events: none; + } + } +} -- cgit From 145ed3797810cc652976a6198d6b23618e570bfd Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 15 Sep 2020 19:27:12 +0200 Subject: Refactor and change logic to only update marker if we reached the last read notification. --- .../flavours/glitch/reducers/notifications.js | 34 +++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index 31d9611a3..33f014dd1 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -55,16 +55,16 @@ const notificationToMap = (state, notification) => ImmutableMap({ }); const normalizeNotification = (state, notification, usePendingItems) => { - const top = !shouldCountUnreadNotifications(state); + const top = state.get('top'); if (usePendingItems || !state.get('pendingItems').isEmpty()) { return state.update('pendingItems', list => list.unshift(notificationToMap(state, notification))).update('unread', unread => unread + 1); } - if (top) { - state = state.set('lastReadId', notification.id); - } else { + if (shouldCountUnreadNotifications(state)) { state = state.update('unread', unread => unread + 1); + } else { + state = state.set('lastReadId', notification.id); } return state.update('items', list => { @@ -77,7 +77,6 @@ const normalizeNotification = (state, notification, usePendingItems) => { }; const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => { - const top = !(shouldCountUnreadNotifications(state)); const lastReadId = state.get('lastReadId'); let items = ImmutableList(); @@ -102,18 +101,19 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece }); } - if (top) { - if (!items.isEmpty()) { - mutable.update('lastReadId', id => compareId(id, items.first().get('id')) > 0 ? id : items.first().get('id')); - } - } else { - mutable.update('unread', unread => unread + items.count(item => compareId(item.get('id'), lastReadId) > 0)); - } - if (!next) { mutable.set('hasMore', false); } + if (shouldCountUnreadNotifications(state)) { + mutable.update('unread', unread => unread + items.count(item => compareId(item.get('id'), lastReadId) > 0)); + } else { + const mostRecentId = items.first().get('id'); + if (compareId(lastReadId, mostRecentId) < 0) { + mutable.set('lastReadId', mostRecentId); + } + } + mutable.set('isLoading', false); }); }; @@ -197,7 +197,13 @@ const updateVisibility = (state, visibility) => { }; const shouldCountUnreadNotifications = (state) => { - return !(state.get('isTabVisible') && state.get('top') && state.get('mounted') > 0); + const isTabVisible = state.get('isTabVisible'); + const isOnTop = state.get('top'); + const isMounted = state.get('mounted') > 0; + const lastReadId = state.get('lastReadId'); + const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (!state.get('items').isEmpty() && compareId(state.get('items').last().get('id'), lastReadId) <= 0); + + return !(isTabVisible && isOnTop && isMounted && lastItemReached); }; const recountUnread = (state, last_read_id) => { -- cgit From 94c290d7d2179adeb0437c00b571f62c41c7fd12 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 15 Sep 2020 20:54:26 +0200 Subject: Only update read marker when giving focus/mounting column --- app/javascript/flavours/glitch/features/notifications/index.js | 4 ++-- app/javascript/flavours/glitch/reducers/notifications.js | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index cef0a0d2a..1c8c68218 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -57,7 +57,7 @@ const mapStateToProps = state => ({ hasMore: state.getIn(['notifications', 'hasMore']), numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), - lastReadId: state.getIn(['notifications', 'lastReadId']), + lastReadId: state.getIn(['notifications', 'readMarkerId']), }); /* glitch */ @@ -227,7 +227,7 @@ class Notifications extends React.PureComponent { accountId={item.get('account')} onMoveUp={this.handleMoveUp} onMoveDown={this.handleMoveDown} - unread={lastReadId && compareId(item.get('id'), lastReadId) > 0} + unread={lastReadId !== '0' && compareId(item.get('id'), lastReadId) > 0} /> )); } else { diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index 33f014dd1..7f6784eae 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -39,6 +39,7 @@ const initialState = ImmutableMap({ mounted: 0, unread: 0, lastReadId: '0', + readMarkerId: '0', isLoading: false, cleaningMode: false, isTabVisible: true, @@ -183,6 +184,7 @@ const deleteMarkedNotifs = (state) => { const updateMounted = (state) => { state = state.update('mounted', count => count + 1); if (!shouldCountUnreadNotifications(state)) { + state = state.set('readMarkerId', state.get('lastReadId')); state = clearUnread(state); } return state; @@ -191,6 +193,7 @@ const updateMounted = (state) => { const updateVisibility = (state, visibility) => { state = state.set('isTabVisible', visibility); if (!shouldCountUnreadNotifications(state)) { + state = state.set('readMarkerId', state.get('lastReadId')); state = clearUnread(state); } return state; @@ -212,6 +215,10 @@ const recountUnread = (state, last_read_id) => { mutable.set('lastReadId', last_read_id); } + if (compareId(last_read_id, mutable.get('readMarkerId')) > 0) { + mutable.set('readMarkerId', 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)); } -- cgit From f1c0cf98061eee5eb6130b167d033c851ee58f14 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 15 Sep 2020 23:42:58 +0200 Subject: Add button to manually mark all notifications as read --- .../flavours/glitch/actions/notifications.js | 8 ++++++ .../glitch/features/notifications/index.js | 32 ++++++++++++++++++++-- .../flavours/glitch/reducers/notifications.js | 4 +++ 3 files changed, 41 insertions(+), 3 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index ceb1e6df6..b44469cf4 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -45,6 +45,8 @@ export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; export const NOTIFICATIONS_SET_VISIBILITY = 'NOTIFICATIONS_SET_VISIBILITY'; +export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; + defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, }); @@ -318,3 +320,9 @@ export function setFilter (filterType) { dispatch(saveSettings()); }; }; + +export function markNotificationsAsRead() { + return { + type: NOTIFICATIONS_MARK_AS_READ, + }; +}; diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index 1c8c68218..d1a0640cb 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -12,6 +12,7 @@ import { mountNotifications, unmountNotifications, loadPending, + markNotificationsAsRead, } from 'flavours/glitch/actions/notifications'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import NotificationContainer from './containers/notification_container'; @@ -31,6 +32,7 @@ import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notifi const messages = defineMessages({ title: { id: 'column.notifications', defaultMessage: 'Notifications' }, enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, + markAsRead : { id: 'notifications.mark_as_read', defaultMessage: 'Mark every notification as read' }, }); const getNotifications = createSelector([ @@ -58,6 +60,7 @@ const mapStateToProps = state => ({ numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), lastReadId: state.getIn(['notifications', 'readMarkerId']), + canMarkAsRead: !state.getIn(['notifications', 'items']).isEmpty() && state.getIn(['notifications', 'readMarkerId']) !== '0' && compareId(state.getIn(['notifications', 'items']).first().get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0, }); /* glitch */ @@ -65,6 +68,9 @@ const mapDispatchToProps = dispatch => ({ onEnterCleaningMode(yes) { dispatch(enterNotificationClearingMode(yes)); }, + onMarkAsRead() { + dispatch(markNotificationsAsRead()); + }, onMount() { dispatch(mountNotifications()); }, @@ -96,6 +102,7 @@ class Notifications extends React.PureComponent { onMount: PropTypes.func, onUnmount: PropTypes.func, lastReadId: PropTypes.string, + canMarkAsRead: PropTypes.bool, }; static defaultProps = { @@ -197,8 +204,12 @@ class Notifications extends React.PureComponent { this.props.onEnterCleaningMode(!this.props.notifCleaningActive); } + handleMarkAsRead = () => { + this.props.onMarkAsRead(); + } + render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId } = this.props; + const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead } = this.props; const { notifCleaning, notifCleaningActive } = this.props; const { animatingNCD } = this.state; const pinned = !!columnId; @@ -256,6 +267,21 @@ class Notifications extends React.PureComponent { ); + const extraButtons = []; + + if (canMarkAsRead) { + extraButtons.push( + + ); + } + const notifCleaningButtonClassName = classNames('column-header__button', { 'active': notifCleaningActive, }); @@ -267,7 +293,7 @@ class Notifications extends React.PureComponent { const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning); - const notifCleaningButton = ( + extraButtons.push(