Added GroupPinnedStatuses
• Added: - GroupPinnedStatuses - controllers for timeline, creation, deletion - redux actions, reducers for creation, deletion - timeline fetching in timelines action - options to pin, unpin in status options popover for group admin
This commit is contained in:
@@ -63,6 +63,14 @@ export const GROUP_UPDATE_ROLE_REQUEST = 'GROUP_UPDATE_ROLE_REQUEST';
|
||||
export const GROUP_UPDATE_ROLE_SUCCESS = 'GROUP_UPDATE_ROLE_SUCCESS';
|
||||
export const GROUP_UPDATE_ROLE_FAIL = 'GROUP_UPDATE_ROLE_FAIL';
|
||||
|
||||
export const GROUP_PIN_STATUS_REQUEST = 'GROUP_PIN_STATUS_REQUEST'
|
||||
export const GROUP_PIN_STATUS_SUCCESS = 'GROUP_PIN_STATUS_SUCCESS'
|
||||
export const GROUP_PIN_STATUS_FAIL = 'GROUP_PIN_STATUS_FAIL'
|
||||
|
||||
export const GROUP_UNPIN_STATUS_REQUEST = 'GROUP_UNPIN_STATUS_REQUEST'
|
||||
export const GROUP_UNPIN_STATUS_SUCCESS = 'GROUP_UNPIN_STATUS_SUCCESS'
|
||||
export const GROUP_UNPIN_STATUS_FAIL = 'GROUP_UNPIN_STATUS_FAIL ='
|
||||
|
||||
export const GROUP_TIMELINE_SORT = 'GROUP_TIMELINE_SORT'
|
||||
export const GROUP_TIMELINE_TOP_SORT = 'GROUP_TIMELINE_TOP_SORT'
|
||||
|
||||
@@ -587,6 +595,83 @@ export function updateRoleFail(groupId, id, error) {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export function pinGroupStatus(groupId, statusId) {
|
||||
return (dispatch, getState) => {
|
||||
if (!me) return
|
||||
|
||||
dispatch(pinGroupStatusRequest(groupId))
|
||||
|
||||
api(getState).post(`/api/v1/groups/${groupId}/pin`, { statusId }).then((response) => {
|
||||
dispatch(pinGroupStatusSuccess(groupId, statusId))
|
||||
}).catch((error) => {
|
||||
dispatch(pinGroupStatusFail(groupId, statusId, error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function pinGroupStatusRequest(groupId) {
|
||||
return {
|
||||
type: GROUP_PIN_STATUS_REQUEST,
|
||||
groupId,
|
||||
}
|
||||
}
|
||||
|
||||
export function pinGroupStatusSuccess(groupId, statusId) {
|
||||
return {
|
||||
type: GROUP_PIN_STATUS_SUCCESS,
|
||||
groupId,
|
||||
statusId,
|
||||
}
|
||||
}
|
||||
|
||||
export function pinGroupStatusFail(groupId, statusId, error) {
|
||||
return {
|
||||
type: GROUP_PIN_STATUS_FAIL,
|
||||
groupId,
|
||||
statusId,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
export function unpinGroupStatus(groupId, statusId) {
|
||||
return (dispatch, getState) => {
|
||||
if (!me) return
|
||||
|
||||
dispatch(unpinGroupStatusRequest(groupId))
|
||||
|
||||
api(getState).post(`/api/v1/groups/${groupId}/unpin`, { statusId }).then((response) => {
|
||||
dispatch(unpinGroupStatusSuccess(groupId, statusId))
|
||||
}).catch((error) => {
|
||||
dispatch(unpinGroupStatusFail(groupId, statusId, error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function unpinGroupStatusRequest(groupId) {
|
||||
return {
|
||||
type: GROUP_UNPIN_STATUS_REQUEST,
|
||||
groupId,
|
||||
}
|
||||
}
|
||||
|
||||
export function unpinGroupStatusSuccess(groupId, statusId) {
|
||||
return {
|
||||
type: GROUP_UNPIN_STATUS_SUCCESS,
|
||||
groupId,
|
||||
statusId,
|
||||
}
|
||||
}
|
||||
|
||||
export function unpinGroupStatusFail(groupId, statusId, error) {
|
||||
return {
|
||||
type: GROUP_UNPIN_STATUS_FAIL,
|
||||
groupId,
|
||||
statusId,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
export const sortGroups = (tab, sortType) => (dispatch, getState) => {
|
||||
const groupIdsByTab = getState().getIn(['group_lists', tab, 'items'], ImmutableList()).toJS()
|
||||
const allGroups = getState().get('groups', ImmutableMap()).toJS()
|
||||
|
||||
@@ -173,6 +173,7 @@ export const expandAccountFeaturedTimeline = accountId => expandTimeline(`accoun
|
||||
export const expandAccountMediaTimeline = (accountId, { maxId, limit, mediaType } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: limit || 20, media_type: mediaType });
|
||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||
export const expandGroupTimeline = (id, { sortBy, maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { sort_by: sortBy, max_id: maxId, only_media: onlyMedia }, done);
|
||||
export const expandGroupFeaturedTimeline = (groupId, done = noOp) => expandTimeline(`group:${groupId}:pinned`, `/api/v1/timelines/group_pins/${groupId}`, {}, done);
|
||||
export const expandGroupCollectionTimeline = (collectionType, { sortBy, maxId } = {}, done = noOp) => expandTimeline(`group_collection:${collectionType}`, `/api/v1/timelines/group_collection/${collectionType}`, { sort_by: sortBy, max_id: maxId }, done);
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
fetchGroupRelationships,
|
||||
createRemovedAccount,
|
||||
groupRemoveStatus,
|
||||
pinGroupStatus,
|
||||
unpinGroupStatus,
|
||||
} from '../../actions/groups'
|
||||
import { initMuteModal } from '../../actions/mutes'
|
||||
import { initReport } from '../../actions/reports'
|
||||
@@ -86,6 +88,10 @@ class StatusOptionsPopover extends ImmutablePureComponent {
|
||||
this.props.onPin(this.props.status)
|
||||
}
|
||||
|
||||
handleGroupPinStatus = () => {
|
||||
this.props.onPinGroupStatus(this.props.status)
|
||||
}
|
||||
|
||||
handleBookmarkClick = () => {
|
||||
if (this.props.isPro) {
|
||||
this.props.onBookmark(this.props.status)
|
||||
@@ -230,6 +236,29 @@ class StatusOptionsPopover extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if (withGroupAdmin) {
|
||||
menu.push(null)
|
||||
menu.push({
|
||||
icon: 'trash',
|
||||
hideArrow: true,
|
||||
title: intl.formatMessage(messages.group_remove_account),
|
||||
onClick: this.handleGroupRemoveAccount,
|
||||
})
|
||||
menu.push({
|
||||
icon: 'trash',
|
||||
hideArrow: true,
|
||||
title: intl.formatMessage(messages.group_remove_post),
|
||||
onClick: this.handleGroupRemovePost,
|
||||
})
|
||||
menu.push(null)
|
||||
menu.push({
|
||||
icon: 'pin',
|
||||
hideArrow: true,
|
||||
title: intl.formatMessage(status.get('pinned_by_group') ? messages.groupUnpin : messages.groupPin),
|
||||
onClick: this.handleGroupPinStatus,
|
||||
})
|
||||
}
|
||||
|
||||
menu.push(null)
|
||||
menu.push({
|
||||
icon: 'copy',
|
||||
@@ -249,22 +278,6 @@ class StatusOptionsPopover extends ImmutablePureComponent {
|
||||
title: intl.formatMessage(messages.embed),
|
||||
onClick: this.handleOnOpenEmbedModal,
|
||||
})
|
||||
|
||||
if (withGroupAdmin) {
|
||||
menu.push(null)
|
||||
menu.push({
|
||||
icon: 'trash',
|
||||
hideArrow: true,
|
||||
title: intl.formatMessage(messages.group_remove_account),
|
||||
onClick: this.handleGroupRemoveAccount,
|
||||
})
|
||||
menu.push({
|
||||
icon: 'trash',
|
||||
hideArrow: true,
|
||||
title: intl.formatMessage(messages.group_remove_post),
|
||||
onClick: this.handleGroupRemovePost,
|
||||
})
|
||||
}
|
||||
|
||||
if (isStaff) {
|
||||
menu.push(null)
|
||||
@@ -316,6 +329,8 @@ const messages = defineMessages({
|
||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||
groupPin: { id: 'status.group_pin', defaultMessage: 'Pin in group' },
|
||||
groupUnpin: { id: 'status.group_unpin', defaultMessage: 'Unpin from group' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark status' },
|
||||
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
|
||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||
@@ -471,6 +486,16 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(openModal(MODAL_PRO_UPGRADE))
|
||||
},
|
||||
|
||||
onPinGroupStatus(status) {
|
||||
dispatch(closePopover())
|
||||
|
||||
if (status.get('pinned_by_group')) {
|
||||
dispatch(unpinGroupStatus(status.getIn(['group', 'id']), status.get('id')))
|
||||
} else {
|
||||
dispatch(pinGroupStatus(status.getIn(['group', 'id']), status.get('id')))
|
||||
}
|
||||
},
|
||||
|
||||
onClosePopover: () => dispatch(closePopover()),
|
||||
})
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ class Status extends ImmutablePureComponent {
|
||||
'isChild',
|
||||
'isPromoted',
|
||||
'isFeatured',
|
||||
'isPinnedInGroup',
|
||||
'isMuted',
|
||||
'isHidden',
|
||||
'isIntersecting',
|
||||
@@ -311,6 +312,7 @@ class Status extends ImmutablePureComponent {
|
||||
const {
|
||||
intl,
|
||||
isFeatured,
|
||||
isPinnedInGroup,
|
||||
isPromoted,
|
||||
isChild,
|
||||
isHidden,
|
||||
@@ -429,7 +431,7 @@ class Status extends ImmutablePureComponent {
|
||||
<div
|
||||
className={[_s.d, _s.outlineNone].join(' ')}
|
||||
tabIndex={this.props.isMuted ? null : 0}
|
||||
data-featured={isFeatured ? 'true' : null}
|
||||
data-featured={(isFeatured || isPinnedInGroup) ? 'true' : null}
|
||||
aria-label={textForScreenReader(intl, status, rebloggedByText)}
|
||||
ref={this.handleRef}
|
||||
onClick={isChild && !isNotification ? this.handleClick : undefined}
|
||||
@@ -442,6 +444,7 @@ class Status extends ImmutablePureComponent {
|
||||
status={this.props.status}
|
||||
isPromoted={isPromoted}
|
||||
isFeatured={isFeatured}
|
||||
isPinnedInGroup={isPinnedInGroup}
|
||||
isComment={isComment && !isChild}
|
||||
/>
|
||||
|
||||
@@ -564,6 +567,7 @@ Status.propTypes = {
|
||||
isChild: PropTypes.bool,
|
||||
isPromoted: PropTypes.bool,
|
||||
isFeatured: PropTypes.bool,
|
||||
isPinnedInGroup: PropTypes.bool,
|
||||
isMuted: PropTypes.bool,
|
||||
isHidden: PropTypes.bool,
|
||||
isIntersecting: PropTypes.bool,
|
||||
|
||||
@@ -67,11 +67,18 @@ class StatusList extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
getFeaturedStatusCount = () => {
|
||||
if (!!this.props.groupPinnedStatusIds) {
|
||||
return this.props.groupPinnedStatusIds.size
|
||||
}
|
||||
|
||||
return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0
|
||||
}
|
||||
|
||||
getCurrentStatusIndex = (id, featured) => {
|
||||
if (featured) {
|
||||
if (!!this.props.groupPinnedStatusIds) {
|
||||
return this.props.groupPinnedStatusIds.indexOf(id)
|
||||
}
|
||||
return this.props.featuredStatusIds.indexOf(id)
|
||||
}
|
||||
|
||||
@@ -129,6 +136,7 @@ class StatusList extends ImmutablePureComponent {
|
||||
const {
|
||||
statusIds,
|
||||
featuredStatusIds,
|
||||
groupPinnedStatusIds,
|
||||
onLoadMore,
|
||||
timelineId,
|
||||
totalQueuedItemsCount,
|
||||
@@ -225,6 +233,20 @@ class StatusList extends ImmutablePureComponent {
|
||||
)).concat(scrollableContent)
|
||||
}
|
||||
|
||||
if (scrollableContent && groupPinnedStatusIds) {
|
||||
scrollableContent = groupPinnedStatusIds.map((statusId) => (
|
||||
<StatusContainer
|
||||
key={`f-${statusId}`}
|
||||
id={statusId}
|
||||
isPinnedInGroup
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType={timelineId}
|
||||
commentsLimited
|
||||
/>
|
||||
)).concat(scrollableContent)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TimelineQueueButtonHeader
|
||||
@@ -326,6 +348,7 @@ StatusList.propTypes = {
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
featuredStatusIds: ImmutablePropTypes.list,
|
||||
groupPinnedStatusIds: ImmutablePropTypes.list,
|
||||
onLoadMore: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
isPartial: PropTypes.bool,
|
||||
|
||||
@@ -16,16 +16,17 @@ class StatusPrepend extends ImmutablePureComponent {
|
||||
isFeatured,
|
||||
isPromoted,
|
||||
isComment,
|
||||
isPinnedInGroup,
|
||||
} = this.props
|
||||
|
||||
if (!status) return null
|
||||
|
||||
const isRepost = (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object')
|
||||
|
||||
if (!isFeatured && !isPromoted && !isRepost && !isComment) return null
|
||||
if (!isFeatured && !isPinnedInGroup && !isPromoted && !isRepost && !isComment) return null
|
||||
|
||||
let iconId
|
||||
if (isFeatured) iconId = 'pin'
|
||||
if (isFeatured || isPinnedInGroup) iconId = 'pin'
|
||||
else if (isPromoted) iconId = 'star'
|
||||
else if (isRepost) iconId = 'repost'
|
||||
else if (isComment) iconId = 'comment'
|
||||
@@ -62,7 +63,7 @@ class StatusPrepend extends ImmutablePureComponent {
|
||||
{
|
||||
!isRepost && !isComment &&
|
||||
<Text color='secondary' size='small'>
|
||||
{intl.formatMessage(isFeatured ? messages.pinned : messages.promoted)}
|
||||
{intl.formatMessage(isFeatured ? messages.pinned : isPinnedInGroup ? messages.pinnedByGroup : messages.promoted)}
|
||||
</Text>
|
||||
}
|
||||
{
|
||||
@@ -99,6 +100,7 @@ const messages = defineMessages({
|
||||
filtered: { id: 'status.filtered', defaultMessage: 'Filtered' },
|
||||
promoted: { id: 'status.promoted', defaultMessage: 'Promoted gab' },
|
||||
pinned: { id: 'status.pinned', defaultMessage: 'Pinned gab' },
|
||||
pinnedByGroup: { id: 'status.pinned_by_group', defaultMessage: 'Pinned to group' },
|
||||
reposted: { id: 'status.reposted_by', defaultMessage: '{name} reposted' },
|
||||
})
|
||||
|
||||
@@ -107,6 +109,7 @@ StatusPrepend.propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
isComment: PropTypes.bool,
|
||||
isFeatured: PropTypes.bool,
|
||||
isPinnedInGroup: PropTypes.bool,
|
||||
isPromoted: PropTypes.bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import { List as ImmutableList } from 'immutable'
|
||||
import { injectIntl, defineMessages } from 'react-intl'
|
||||
import { me } from '../initial_state'
|
||||
import getSortBy from '../utils/group_sort_by'
|
||||
@@ -10,6 +11,7 @@ import { connectGroupStream } from '../actions/streaming'
|
||||
import {
|
||||
clearTimeline,
|
||||
expandGroupTimeline,
|
||||
expandGroupFeaturedTimeline,
|
||||
} from '../actions/timelines'
|
||||
import {
|
||||
setGroupTimelineSort,
|
||||
@@ -41,6 +43,8 @@ class GroupTimeline extends ImmutablePureComponent {
|
||||
this.props.setMemberNewest()
|
||||
} else {
|
||||
const sortBy = getSortBy(sortByValue, sortByTopValue, onlyMedia)
|
||||
|
||||
this.props.onExpandGroupFeaturedTimeline(groupId)
|
||||
this.props.onExpandGroupTimeline(groupId, { sortBy, onlyMedia })
|
||||
|
||||
if (!!me) {
|
||||
@@ -57,6 +61,10 @@ class GroupTimeline extends ImmutablePureComponent {
|
||||
this.handleLoadMore()
|
||||
this.props.onClearTimeline(`group:${this.props.groupId}`)
|
||||
}
|
||||
|
||||
if (prevProps.groupId !== this.props.groupId) {
|
||||
this.props.onExpandGroupFeaturedTimeline(this.props.groupId)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -83,6 +91,7 @@ class GroupTimeline extends ImmutablePureComponent {
|
||||
group,
|
||||
groupId,
|
||||
intl,
|
||||
groupPinnedStatusIds,
|
||||
} = this.props
|
||||
|
||||
if (typeof group === 'undefined') {
|
||||
@@ -98,6 +107,7 @@ class GroupTimeline extends ImmutablePureComponent {
|
||||
scrollKey={`group-timeline-${groupId}`}
|
||||
timelineId={`group:${groupId}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
groupPinnedStatusIds={groupPinnedStatusIds}
|
||||
emptyMessage={intl.formatMessage(messages.empty)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
@@ -110,9 +120,12 @@ const messages = defineMessages({
|
||||
empty: { id: 'empty_column.group', defaultMessage: 'There is nothing in this group yet.\nWhen members of this group post new statuses, they will appear here.' },
|
||||
})
|
||||
|
||||
const emptyList = ImmutableList()
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
groupId: props.params.id,
|
||||
group: state.getIn(['groups', props.params.id]),
|
||||
groupPinnedStatusIds: state.getIn(['timelines', `group:${props.params.id}:pinned`, 'items'], emptyList),
|
||||
sortByValue: state.getIn(['group_lists', 'sortByValue']),
|
||||
sortByTopValue: state.getIn(['group_lists', 'sortByTopValue']),
|
||||
})
|
||||
@@ -130,6 +143,9 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
setMemberNewest() {
|
||||
dispatch(setGroupTimelineSort(GROUP_TIMELINE_SORTING_TYPE_NEWEST))
|
||||
},
|
||||
onExpandGroupFeaturedTimeline(groupId) {
|
||||
dispatch(expandGroupFeaturedTimeline(groupId))
|
||||
},
|
||||
})
|
||||
|
||||
GroupTimeline.propTypes = {
|
||||
@@ -143,6 +159,7 @@ GroupTimeline.propTypes = {
|
||||
onConnectGroupStream: PropTypes.func.isRequired,
|
||||
onClearTimeline: PropTypes.func.isRequired,
|
||||
onExpandGroupTimeline: PropTypes.func.isRequired,
|
||||
onExpandGroupFeaturedTimeline: PropTypes.func.isRequired,
|
||||
setMemberNewest: PropTypes.func.isRequired,
|
||||
sortByValue: PropTypes.string.isRequired,
|
||||
sortByTopValue: PropTypes.string,
|
||||
|
||||
Reference in New Issue
Block a user