Added comment sorting

• Added:
- comment sorting to comments on status page
- new comment sorting options popover, web setting, constants
This commit is contained in:
mgabdev 2020-05-27 01:15:10 -04:00
parent 0364a4000b
commit 260553b7e7
8 changed files with 210 additions and 30 deletions

View File

@ -27,7 +27,7 @@ const debouncedSave = debounce((dispatch, getState) => {
api().put('/api/web/settings', { data }) api().put('/api/web/settings', { data })
.then(() => dispatch({ type: SETTING_SAVE })) .then(() => dispatch({ type: SETTING_SAVE }))
.catch(() => { /* */ }) .catch(() => { /* */ })
}, 500, { trailing: true }) }, 50, { trailing: true })
export function saveSettings() { export function saveSettings() {
return (dispatch, getState) => debouncedSave(dispatch, getState) return (dispatch, getState) => debouncedSave(dispatch, getState)

View File

@ -0,0 +1,71 @@
import { defineMessages, injectIntl } from 'react-intl'
import { closePopover } from '../../actions/popover'
import { changeSetting, saveSettings } from '../../actions/settings'
import {
COMMENT_SORTING_TYPE_NEWEST,
COMMENT_SORTING_TYPE_OLDEST,
COMMENT_SORTING_TYPE_TOP,
} from '../../constants'
import PopoverLayout from './popover_layout'
import List from '../list'
const messages = defineMessages({
oldest: { id: 'comment_sort.oldest', defaultMessage: 'Oldest' },
newest: { id: 'comment_sort.newest', defaultMessage: 'Recent' },
top: { id: 'comment_sort.top', defaultMessage: 'Most Liked' },
})
const mapDispatchToProps = (dispatch) => ({
onSetCommentSortingSetting(type) {
dispatch(changeSetting(['commentSorting'], type))
dispatch(saveSettings())
dispatch(closePopover())
},
})
export default
@injectIntl
@connect(null, mapDispatchToProps)
class CommentSortingOptionsPopover extends PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
onSetCommentSortingSetting: PropTypes.func.isRequired,
isXS: PropTypes.bool,
}
handleOnClick = (type) => {
this.props.onSetCommentSortingSetting(type)
}
render() {
const { intl, isXS } = this.props
return (
<PopoverLayout width={180} isXS={isXS}>
<List
size='large'
scrollKey='comment_sorting_options'
items={[
{
hideArrow: true,
title: intl.formatMessage(messages.newest),
onClick: () => this.handleOnClick(COMMENT_SORTING_TYPE_NEWEST),
},
{
hideArrow: true,
title: intl.formatMessage(messages.oldest),
onClick: () => this.handleOnClick(COMMENT_SORTING_TYPE_OLDEST),
},
{
hideArrow: true,
title: intl.formatMessage(messages.top),
onClick: () => this.handleOnClick(COMMENT_SORTING_TYPE_TOP),
},
]}
small
/>
</PopoverLayout>
)
}
}

View File

@ -1,29 +1,27 @@
import { import {
BREAKPOINT_EXTRA_SMALL, BREAKPOINT_EXTRA_SMALL,
POPOVER_COMMENT_SORTING_OPTIONS,
POPOVER_DATE_PICKER, POPOVER_DATE_PICKER,
POPOVER_EMOJI_PICKER, POPOVER_EMOJI_PICKER,
POPOVER_GROUP_OPTIONS, POPOVER_GROUP_OPTIONS,
POPOVER_NAV_SETTINGS, POPOVER_NAV_SETTINGS,
POPOVER_PROFILE_OPTIONS, POPOVER_PROFILE_OPTIONS,
POPOVER_REPOST_OPTIONS,
POPOVER_SEARCH, POPOVER_SEARCH,
POPOVER_SIDEBAR_MORE, POPOVER_SIDEBAR_MORE,
POPOVER_STATUS_OPTIONS, POPOVER_STATUS_OPTIONS,
POPOVER_STATUS_SHARE,
POPOVER_STATUS_VISIBILITY, POPOVER_STATUS_VISIBILITY,
POPOVER_USER_INFO, POPOVER_USER_INFO,
} from '../../constants' } from '../../constants'
import { import {
CommentSortingOptionsPopover,
DatePickerPopover, DatePickerPopover,
EmojiPickerPopover, EmojiPickerPopover,
GroupOptionsPopover, GroupOptionsPopover,
NavSettingsPopover, NavSettingsPopover,
ProfileOptionsPopover, ProfileOptionsPopover,
RepostOptionsPopover,
SearchPopover, SearchPopover,
SidebarMorePopover, SidebarMorePopover,
StatusOptionsPopover, StatusOptionsPopover,
StatusSharePopover,
StatusVisibilityPopover, StatusVisibilityPopover,
UserInfoPopover, UserInfoPopover,
} from '../../features/ui/util/async_components' } from '../../features/ui/util/async_components'
@ -37,16 +35,15 @@ import PopoverBase from './popover_base'
const initialState = getWindowDimension() const initialState = getWindowDimension()
const POPOVER_COMPONENTS = {} const POPOVER_COMPONENTS = {}
POPOVER_COMPONENTS[POPOVER_COMMENT_SORTING_OPTIONS] = CommentSortingOptionsPopover
POPOVER_COMPONENTS[POPOVER_DATE_PICKER] = DatePickerPopover POPOVER_COMPONENTS[POPOVER_DATE_PICKER] = DatePickerPopover
POPOVER_COMPONENTS[POPOVER_EMOJI_PICKER] = EmojiPickerPopover POPOVER_COMPONENTS[POPOVER_EMOJI_PICKER] = EmojiPickerPopover
POPOVER_COMPONENTS[POPOVER_GROUP_OPTIONS] = GroupOptionsPopover POPOVER_COMPONENTS[POPOVER_GROUP_OPTIONS] = GroupOptionsPopover
POPOVER_COMPONENTS[POPOVER_NAV_SETTINGS] = NavSettingsPopover POPOVER_COMPONENTS[POPOVER_NAV_SETTINGS] = NavSettingsPopover
POPOVER_COMPONENTS[POPOVER_PROFILE_OPTIONS] = ProfileOptionsPopover POPOVER_COMPONENTS[POPOVER_PROFILE_OPTIONS] = ProfileOptionsPopover
POPOVER_COMPONENTS[POPOVER_REPOST_OPTIONS] = RepostOptionsPopover
POPOVER_COMPONENTS[POPOVER_SEARCH] = SearchPopover POPOVER_COMPONENTS[POPOVER_SEARCH] = SearchPopover
POPOVER_COMPONENTS[POPOVER_SIDEBAR_MORE] = SidebarMorePopover POPOVER_COMPONENTS[POPOVER_SIDEBAR_MORE] = SidebarMorePopover
POPOVER_COMPONENTS[POPOVER_STATUS_OPTIONS] = StatusOptionsPopover POPOVER_COMPONENTS[POPOVER_STATUS_OPTIONS] = StatusOptionsPopover
POPOVER_COMPONENTS[POPOVER_STATUS_SHARE] = StatusSharePopover
POPOVER_COMPONENTS[POPOVER_STATUS_VISIBILITY] = StatusVisibilityPopover POPOVER_COMPONENTS[POPOVER_STATUS_VISIBILITY] = StatusVisibilityPopover
POPOVER_COMPONENTS[POPOVER_USER_INFO] = UserInfoPopover POPOVER_COMPONENTS[POPOVER_USER_INFO] = UserInfoPopover

View File

@ -1,25 +1,36 @@
import { Fragment } from 'react'
import ImmutablePropTypes from 'react-immutable-proptypes' import ImmutablePropTypes from 'react-immutable-proptypes'
import { injectIntl } from 'react-intl' import { defineMessages, injectIntl } from 'react-intl'
import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePureComponent from 'react-immutable-pure-component'
import { HotKeys } from 'react-hotkeys' import { HotKeys } from 'react-hotkeys'
import classNames from 'classnames/bind' import {
CX,
COMMENT_SORTING_TYPE_NEWEST,
COMMENT_SORTING_TYPE_TOP,
} from '../constants'
import { me, displayMedia, compactMode } from '../initial_state' import { me, displayMedia, compactMode } from '../initial_state'
import scheduleIdleTask from '../utils/schedule_idle_task' import scheduleIdleTask from '../utils/schedule_idle_task'
import ComposeFormContainer from '../features/compose/containers/compose_form_container' import ComposeFormContainer from '../features/compose/containers/compose_form_container'
import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component' import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component'
import ColumnIndicator from './column_indicator'
import StatusContent from './status_content' import StatusContent from './status_content'
import StatusPrepend from './status_prepend' import StatusPrepend from './status_prepend'
import StatusActionBar from './status_action_bar' import StatusActionBar from './status_action_bar'
import StatusMedia from './status_media' import StatusMedia from './status_media'
import StatusHeader from './status_header' import StatusHeader from './status_header'
import CommentList from './comment_list' import CommentList from './comment_list'
import Button from './button'
import Text from './text'
// We use the component (and not the container) since we do not want // We use the component (and not the container) since we do not want
// to use the progress bar to show download progress // to use the progress bar to show download progress
import Bundle from '../features/ui/util/bundle' import Bundle from '../features/ui/util/bundle'
const cx = classNames.bind(_s) const messages = defineMessages({
sortBy: { id: 'comment_sort.title', defaultMessage: 'Sort by' },
oldest: { id: 'comment_sort.oldest', defaultMessage: 'Oldest' },
newest: { id: 'comment_sort.newest', defaultMessage: 'Recent' },
top: { id: 'comment_sort.top', defaultMessage: 'Most Liked' },
})
export const textForScreenReader = (intl, status, rebloggedByText = false) => { export const textForScreenReader = (intl, status, rebloggedByText = false) => {
if (!intl || !status) return '' if (!intl || !status) return ''
@ -97,7 +108,9 @@ class Status extends ImmutablePureComponent {
commentsLimited: PropTypes.bool, commentsLimited: PropTypes.bool,
onOpenLikes: PropTypes.func.isRequired, onOpenLikes: PropTypes.func.isRequired,
onOpenReposts: PropTypes.func.isRequired, onOpenReposts: PropTypes.func.isRequired,
onCommentSortOpen: PropTypes.func.isRequired,
isComposeModalOpen: PropTypes.bool, isComposeModalOpen: PropTypes.bool,
commentSortingType: PropTypes.string,
} }
// Avoid checking props that are functions (and whose equality will always // Avoid checking props that are functions (and whose equality will always
@ -112,6 +125,7 @@ class Status extends ImmutablePureComponent {
'isHidden', 'isHidden',
'isIntersecting', 'isIntersecting',
'isComment', 'isComment',
'commentSortingType',
] ]
state = { state = {
@ -361,11 +375,19 @@ class Status extends ImmutablePureComponent {
} }
} }
handleOnCommentSortOpen = () => {
this.props.onCommentSortOpen(this.commentSortButtonRef)
}
handleRef = c => { handleRef = c => {
this.node = c this.node = c
this._measureHeight() this._measureHeight()
} }
setCommentSortButtonRef = (c) => {
this.commentSortButtonRef = c
}
render() { render() {
const { const {
intl, intl,
@ -380,6 +402,7 @@ class Status extends ImmutablePureComponent {
isComment, isComment,
contextType, contextType,
isComposeModalOpen, isComposeModalOpen,
commentSortingType,
} = this.props } = this.props
// const { height } = this.state // const { height } = this.state
@ -389,7 +412,6 @@ class Status extends ImmutablePureComponent {
if (isComment && !ancestorStatus && !isChild) { if (isComment && !ancestorStatus && !isChild) {
// Wait to load... // Wait to load...
// return <ColumnIndicator type='loading' />
return null return null
} }
@ -408,6 +430,13 @@ class Status extends ImmutablePureComponent {
} }
} }
let sortByTitle = intl.formatMessage(messages.oldest)
if (commentSortingType === COMMENT_SORTING_TYPE_NEWEST) {
sortByTitle = intl.formatMessage(messages.newest)
} else if (commentSortingType === COMMENT_SORTING_TYPE_TOP) {
sortByTitle = intl.formatMessage(messages.top)
}
const handlers = (this.props.isMuted || isChild) ? {} : { const handlers = (this.props.isMuted || isChild) ? {} : {
reply: this.handleHotkeyReply, reply: this.handleHotkeyReply,
favorite: this.handleHotkeyFavorite, favorite: this.handleHotkeyFavorite,
@ -421,11 +450,11 @@ class Status extends ImmutablePureComponent {
toggleSensitive: this.handleHotkeyToggleSensitive, toggleSensitive: this.handleHotkeyToggleSensitive,
} }
const parentClasses = cx({ const parentClasses = CX({
pb15: !isChild && !compactMode, pb15: !isChild && !compactMode,
}) })
const containerClasses = cx({ const containerClasses = CX({
default: 1, default: 1,
radiusSmall: !isChild && !compactMode, radiusSmall: !isChild && !compactMode,
bgPrimary: !isChild, bgPrimary: !isChild,
@ -436,7 +465,7 @@ class Status extends ImmutablePureComponent {
borderColorSecondary: !isChild && compactMode, borderColorSecondary: !isChild && compactMode,
}) })
const containerClassesXS = cx({ const containerClassesXS = CX({
default: 1, default: 1,
bgPrimary: !isChild, bgPrimary: !isChild,
boxShadowBlock: !isChild && !compactMode, boxShadowBlock: !isChild && !compactMode,
@ -445,7 +474,7 @@ class Status extends ImmutablePureComponent {
borderColorSecondary: !isChild, borderColorSecondary: !isChild,
}) })
const innerContainerClasses = cx({ const innerContainerClasses = CX({
default: 1, default: 1,
overflowHidden: 1, overflowHidden: 1,
radiusSmall: isChild, radiusSmall: isChild,
@ -560,17 +589,36 @@ class Status extends ImmutablePureComponent {
{ {
descendantsIds && !compactMode && !isChild && descendantsIds.size > 0 && descendantsIds && !compactMode && !isChild && descendantsIds.size > 0 &&
<Fragment>
<div className={[_s.default, _s.mr10, _s.ml10, _s.mb10, _s.borderColorSecondary, _s.borderBottom1PX].join(' ')} /> <div className={[_s.default, _s.mr10, _s.ml10, _s.mb10, _s.borderColorSecondary, _s.borderBottom1PX].join(' ')} />
}
{ {
descendantsIds && !compactMode && !isChild && descendantsIds.size > 0 && !commentsLimited &&
<div className={[_s.default, _s.px15, _s.py5, _s.mb5, _s.flexRow].join(' ')}>
<Text color='secondary' size='small'>
{intl.formatMessage(messages.sortBy)}
</Text>
<Button
isText
backgroundColor='none'
color='secondary'
className={_s.ml5}
buttonRef={this.setCommentSortButtonRef}
onClick={this.handleOnCommentSortOpen}
>
<Text color='inherit' weight='medium' size='small'>
{sortByTitle}
</Text>
</Button>
</div>
}
<CommentList <CommentList
ancestorAccountId={status.getIn(['account', 'id'])} ancestorAccountId={status.getIn(['account', 'id'])}
commentsLimited={commentsLimited} commentsLimited={commentsLimited}
descendants={descendantsIds} descendants={descendantsIds}
onViewComments={this.handleClick} onViewComments={this.handleClick}
/> />
</Fragment>
} }
</div> </div>
</div> </div>

View File

@ -19,6 +19,7 @@ export const URL_GAB_PRO = 'https://pro.gab.com'
export const PLACEHOLDER_MISSING_HEADER_SRC = '/original/missing.png' export const PLACEHOLDER_MISSING_HEADER_SRC = '/original/missing.png'
export const POPOVER_COMMENT_SORTING_OPTIONS = 'COMMENT_SORTING_OPTIONS'
export const POPOVER_DATE_PICKER = 'DATE_PICKER' export const POPOVER_DATE_PICKER = 'DATE_PICKER'
export const POPOVER_EMOJI_PICKER = 'EMOJI_PICKER' export const POPOVER_EMOJI_PICKER = 'EMOJI_PICKER'
export const POPOVER_GROUP_OPTIONS = 'GROUP_OPTIONS' export const POPOVER_GROUP_OPTIONS = 'GROUP_OPTIONS'
@ -63,6 +64,10 @@ export const MODAL_UNAUTHORIZED = 'UNAUTHORIZED'
export const MODAL_UNFOLLOW = 'UNFOLLOW' export const MODAL_UNFOLLOW = 'UNFOLLOW'
export const MODAL_VIDEO = 'VIDEO' export const MODAL_VIDEO = 'VIDEO'
export const COMMENT_SORTING_TYPE_NEWEST = 'newest'
export const COMMENT_SORTING_TYPE_OLDEST = 'oldest'
export const COMMENT_SORTING_TYPE_TOP = 'most-liked'
export const FONT_SIZES_EXTRA_SMALL = '12px' export const FONT_SIZES_EXTRA_SMALL = '12px'
export const FONT_SIZES_SMALL = '13px' export const FONT_SIZES_SMALL = '13px'
export const FONT_SIZES_NORMAL = '14px' export const FONT_SIZES_NORMAL = '14px'

View File

@ -30,26 +30,69 @@ import {
import { import {
MODAL_BOOST, MODAL_BOOST,
MODAL_CONFIRM, MODAL_CONFIRM,
POPOVER_COMMENT_SORTING_OPTIONS,
COMMENT_SORTING_TYPE_OLDEST,
COMMENT_SORTING_TYPE_NEWEST,
COMMENT_SORTING_TYPE_TOP,
} from '../constants' } from '../constants'
import { makeGetStatus } from '../selectors' import { makeGetStatus } from '../selectors'
import Status from '../components/status'; import Status from '../components/status';
const getDescendants = (state, status, highlightStatusId) => {
const sortReplies = (replyIds, state, type) => {
if (!replyIds) return replyIds
if (type === COMMENT_SORTING_TYPE_OLDEST || !type) {
return replyIds // default
} else if (type === COMMENT_SORTING_TYPE_NEWEST) {
return replyIds.reverse()
} else if (type === COMMENT_SORTING_TYPE_TOP) {
let statusList = []
replyIds.forEach((replyId) => {
const status = state.getIn(['statuses', replyId])
if (status) {
statusList.push({
id: replyId,
likeCount: status.get('favourites_count'),
})
}
})
statusList.sort((a, b) => parseFloat(b.likeCount) - parseFloat(a.likeCount))
let newReplyIds = ImmutableList()
for (let i = 0; i < statusList.length; i++) {
const block = statusList[i];
newReplyIds = newReplyIds.set(i, block.id)
}
return newReplyIds
}
return replyIds
}
const getDescendants = (state, status, highlightStatusId, commentSortingType) => {
let descendantsIds = ImmutableList() let descendantsIds = ImmutableList()
let indent = -1 let indent = -1
let index = 0
descendantsIds = descendantsIds.withMutations(mutable => { descendantsIds = descendantsIds.withMutations(mutable => {
const ids = [status.get('id')] const ids = [status.get('id')]
while (ids.length > 0) { while (ids.length > 0) {
let id = ids.shift() let id = ids.shift()
const replies = state.getIn(['contexts', 'replies', id])
let replies = state.getIn(['contexts', 'replies', id])
// Sort only Top-level replies
if (index === 0) {
replies = sortReplies(replies, state, commentSortingType)
}
if (status.get('id') !== id) { if (status.get('id') !== id) {
mutable.push(ImmutableMap({ mutable.push(ImmutableMap({
statusId: id, statusId: id,
indent: indent, indent: indent,
isHighlighted: highlightStatusId === id, isHighlighted: !!highlightStatusId && highlightStatusId === id,
})) }))
} }
@ -62,6 +105,8 @@ const getDescendants = (state, status, highlightStatusId) => {
} else { } else {
indent = 0 // reset indent = 0 // reset
} }
index++
} }
}) })
@ -74,6 +119,7 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const statusId = props.id || props.params.statusId const statusId = props.id || props.params.statusId
const username = props.params ? props.params.username : undefined const username = props.params ? props.params.username : undefined
const commentSortingType = state.getIn(['settings', 'commentSorting'])
const status = getStatus(state, { const status = getStatus(state, {
id: statusId, id: statusId,
@ -113,7 +159,7 @@ const makeMapStateToProps = () => {
// //
if (status && status.get('replies_count') > 0 && !fetchedContext) { if (status && status.get('replies_count') > 0 && !fetchedContext) {
descendantsIds = getDescendants(state, status) descendantsIds = getDescendants(state, status, null, commentSortingType)
} }
const isComment = !!status ? !!status.get('in_reply_to_id') : false const isComment = !!status ? !!status.get('in_reply_to_id') : false
@ -123,6 +169,7 @@ const makeMapStateToProps = () => {
ancestorStatus, ancestorStatus,
descendantsIds, descendantsIds,
isComment, isComment,
commentSortingType,
isComposeModalOpen: state.getIn(['modal', 'modalType']) === 'COMPOSE', isComposeModalOpen: state.getIn(['modal', 'modalType']) === 'COMPOSE',
} }
} }
@ -243,6 +290,15 @@ const mapDispatchToProps = (dispatch) => ({
} }
} }
}, },
onCommentSortOpen(targetRef, callback) {
dispatch(openPopover(POPOVER_COMMENT_SORTING_OPTIONS, {
targetRef,
callback,
position: 'top',
}))
}
}); });
export default connect(makeMapStateToProps, mapDispatchToProps)(Status); export default connect(makeMapStateToProps, mapDispatchToProps)(Status);

View File

@ -6,6 +6,7 @@ export function BlockDomainModal() { return import(/* webpackChunkName: "compone
export function BlockedAccounts() { return import(/* webpackChunkName: "features/blocked_accounts" */'../../blocked_accounts') } export function BlockedAccounts() { return import(/* webpackChunkName: "features/blocked_accounts" */'../../blocked_accounts') }
export function BlockedDomains() { return import(/* webpackChunkName: "features/blocked_domains" */'../../blocked_domains') } export function BlockedDomains() { return import(/* webpackChunkName: "features/blocked_domains" */'../../blocked_domains') }
export function BoostModal() { return import(/* webpackChunkName: "components/boost_modal" */'../../../components/modal/boost_modal') } export function BoostModal() { return import(/* webpackChunkName: "components/boost_modal" */'../../../components/modal/boost_modal') }
export function CommentSortingOptionsPopover() { return import(/* webpackChunkName: "components/comment_sorting_options_popover" */'../../../components/popover/comment_sorting_options_popover') }
export function CommunityTimeline() { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline') } export function CommunityTimeline() { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline') }
export function CommunityTimelineSettingsModal() { return import(/* webpackChunkName: "components/community_timeline_settings_modal" */'../../../components/modal/community_timeline_settings_modal') } export function CommunityTimelineSettingsModal() { return import(/* webpackChunkName: "components/community_timeline_settings_modal" */'../../../components/modal/community_timeline_settings_modal') }
export function Compose() { return import(/* webpackChunkName: "features/compose" */'../../compose') } export function Compose() { return import(/* webpackChunkName: "features/compose" */'../../compose') }

View File

@ -2,6 +2,7 @@ import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings'
import { STORE_HYDRATE } from '../actions/store' import { STORE_HYDRATE } from '../actions/store'
import { EMOJI_USE } from '../actions/emojis' import { EMOJI_USE } from '../actions/emojis'
import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists' import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists'
import { COMMENT_SORTING_TYPE_OLDEST } from '../constants'
import { Map as ImmutableMap, fromJS } from 'immutable' import { Map as ImmutableMap, fromJS } from 'immutable'
import uuid from '../utils/uuid' import uuid from '../utils/uuid'
@ -9,6 +10,7 @@ const initialState = ImmutableMap({
saved: true, saved: true,
onboarded: false, onboarded: false,
skinTone: 1, skinTone: 1,
commentSorting: COMMENT_SORTING_TYPE_OLDEST,
displayOptions: ImmutableMap({ displayOptions: ImmutableMap({
fontSize: 'normal', fontSize: 'normal',