mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-27 13:16:07 +00:00
[WIP] Initial status work
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
// <ListConversationContainer>
|
||||
// =================
|
||||
|
||||
// For code documentation, please see:
|
||||
// https://glitch-soc.github.io/docs/javascript/glitch/list/conversation/container
|
||||
|
||||
// For more information, please contact:
|
||||
// @kibi@glitch.social
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// Imports
|
||||
// -------
|
||||
|
||||
// Package imports.
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
// Mastodon imports.
|
||||
import { fetchContext } from 'mastodon/actions/statuses';
|
||||
|
||||
// Our imports.
|
||||
import ListConversation from '.';
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// State mapping
|
||||
// -------------
|
||||
|
||||
const mapStateToProps = (state, { id }) => {
|
||||
return {
|
||||
ancestors : state.getIn(['contexts', 'ancestors', id]),
|
||||
descendants : state.getIn(['contexts', 'descendants', id]),
|
||||
};
|
||||
};
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// Dispatch mapping
|
||||
// ----------------
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
fetch (id) {
|
||||
dispatch(fetchContext(id));
|
||||
},
|
||||
});
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// Connecting
|
||||
// ----------
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ListConversation);
|
||||
80
app/javascript/glitch/components/list/conversation/index.js
Normal file
80
app/javascript/glitch/components/list/conversation/index.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// <ListConversation>
|
||||
// ====================
|
||||
|
||||
// For code documentation, please see:
|
||||
// https://glitch-soc.github.io/docs/javascript/glitch/list/conversation
|
||||
|
||||
// For more information, please contact:
|
||||
// @kibi@glitch.social
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// Imports
|
||||
// -------
|
||||
|
||||
// Package imports.
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ScrollContainer from 'react-router-scroll';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
// Our imports.
|
||||
import StatusContainer from 'glitch/components/status/container';
|
||||
|
||||
// Stylesheet imports.
|
||||
import './style';
|
||||
|
||||
// * * * * * * * //
|
||||
|
||||
// The component
|
||||
// -------------
|
||||
|
||||
export default class ListConversation extends ImmutablePureComponent {
|
||||
|
||||
// Props.
|
||||
static propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
ancestors: ImmutablePropTypes.list,
|
||||
descendants: ImmutablePropTypes.list,
|
||||
fetch: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
// If this is a detailed status, we should fetch its contents and
|
||||
// context upon mounting.
|
||||
componentWillMount () {
|
||||
const { id, fetch } = this.props;
|
||||
fetch(id);
|
||||
}
|
||||
|
||||
// Similarly, if the component receives new props, we need to fetch
|
||||
// the new status.
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const { id, fetch } = this.props;
|
||||
if (nextProps.id !== id) fetch(nextProps.id);
|
||||
}
|
||||
|
||||
// We just render our status inside a column with its
|
||||
// ancestors and decendants.
|
||||
render () {
|
||||
const { id, ancestors, descendants } = this.props;
|
||||
return (
|
||||
<ScrollContainer scrollKey='thread'>
|
||||
<div className='glitch glitch__list__conversation scrollable'>
|
||||
{ancestors && ancestors.size > 0 ? (
|
||||
ancestors.map(
|
||||
ancestor => <StatusContainer key={ancestor} id={ancestor} route />
|
||||
)
|
||||
) : null}
|
||||
<StatusContainer key={id} id={id} detailed route />
|
||||
{descendants && descendants.size > 0 ? (
|
||||
descendants.map(
|
||||
descendant => <StatusContainer key={descendant} id={descendant} route />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
|
||||
`<NotificationPurgeButtonsContainer>`
|
||||
=========================
|
||||
|
||||
This container connects `<NotificationPurgeButtons>`s to the Redux store.
|
||||
|
||||
*/
|
||||
|
||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
|
||||
/*
|
||||
|
||||
Imports:
|
||||
--------
|
||||
|
||||
*/
|
||||
|
||||
// Package imports //
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
// Our imports //
|
||||
import NotificationPurgeButtons from './notification_purge_buttons';
|
||||
import {
|
||||
deleteMarkedNotifications,
|
||||
enterNotificationClearingMode,
|
||||
markAllNotifications,
|
||||
} from '../../../../mastodon/actions/notifications';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { openModal } from '../../../../mastodon/actions/modal';
|
||||
|
||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
|
||||
/*
|
||||
|
||||
Dispatch mapping:
|
||||
-----------------
|
||||
|
||||
The `mapDispatchToProps()` function maps dispatches to our store to the
|
||||
various props of our component. We only need to provide a dispatch for
|
||||
deleting notifications.
|
||||
|
||||
*/
|
||||
|
||||
const messages = defineMessages({
|
||||
clearMessage: { id: 'notifications.marked_clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all selected notifications?' },
|
||||
clearConfirm: { id: 'notifications.marked_clear', defaultMessage: 'Clear selected notifications' },
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onEnterCleaningMode(yes) {
|
||||
dispatch(enterNotificationClearingMode(yes));
|
||||
},
|
||||
|
||||
onDeleteMarked() {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.clearMessage),
|
||||
confirm: intl.formatMessage(messages.clearConfirm),
|
||||
onConfirm: () => dispatch(deleteMarkedNotifications()),
|
||||
}));
|
||||
},
|
||||
|
||||
onMarkAll() {
|
||||
dispatch(markAllNotifications(true));
|
||||
},
|
||||
|
||||
onMarkNone() {
|
||||
dispatch(markAllNotifications(false));
|
||||
},
|
||||
|
||||
onInvert() {
|
||||
dispatch(markAllNotifications(null));
|
||||
},
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
markNewForDelete: state.getIn(['notifications', 'markNewForDelete']),
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationPurgeButtons));
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Buttons widget for controlling the notification clearing mode.
|
||||
* In idle state, the cleaning mode button is shown. When the mode is active,
|
||||
* a Confirm and Abort buttons are shown in its place.
|
||||
*/
|
||||
|
||||
|
||||
// Package imports //
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
// Mastodon imports //
|
||||
|
||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
|
||||
const messages = defineMessages({
|
||||
btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' },
|
||||
btnNone : { id: 'notification_purge.btn_none', defaultMessage: 'Select\nnone' },
|
||||
btnInvert : { id: 'notification_purge.btn_invert', defaultMessage: 'Invert\nselection' },
|
||||
btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' },
|
||||
});
|
||||
|
||||
@injectIntl
|
||||
export default class NotificationPurgeButtons extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
onDeleteMarked : PropTypes.func.isRequired,
|
||||
onMarkAll : PropTypes.func.isRequired,
|
||||
onMarkNone : PropTypes.func.isRequired,
|
||||
onInvert : PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
markNewForDelete: PropTypes.bool,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, markNewForDelete } = this.props;
|
||||
|
||||
//className='active'
|
||||
return (
|
||||
<div className='column-header__notif-cleaning-buttons'>
|
||||
<button onClick={this.props.onMarkAll} className={markNewForDelete ? 'active' : ''}>
|
||||
<b>∀</b><br />{intl.formatMessage(messages.btnAll)}
|
||||
</button>
|
||||
|
||||
<button onClick={this.props.onMarkNone} className={!markNewForDelete ? 'active' : ''}>
|
||||
<b>∅</b><br />{intl.formatMessage(messages.btnNone)}
|
||||
</button>
|
||||
|
||||
<button onClick={this.props.onInvert}>
|
||||
<b>¬</b><br />{intl.formatMessage(messages.btnInvert)}
|
||||
</button>
|
||||
|
||||
<button onClick={this.props.onDeleteMarked}>
|
||||
<i className='fa fa-trash' /><br />{intl.formatMessage(messages.btnApply)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
209
app/javascript/glitch/components/list/statuses/index.js
Normal file
209
app/javascript/glitch/components/list/statuses/index.js
Normal file
@@ -0,0 +1,209 @@
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { ScrollContainer } from 'react-router-scroll';
|
||||
import PropTypes from 'prop-types';
|
||||
import IntersectionObserverWrapper from 'mastodon/features/ui/util/intersection_observer_wrapper';
|
||||
import { throttle } from 'lodash';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import StatusContainer from 'glitch/components/status/container';
|
||||
import CommonButton from 'glitch/components/common/button';
|
||||
|
||||
const messages = defineMessages({
|
||||
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
|
||||
});
|
||||
|
||||
@injectIntl
|
||||
export default class ListStatuses extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
onScrollToBottom: PropTypes.func,
|
||||
onScrollToTop: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
trackScroll: PropTypes.bool,
|
||||
shouldUpdateScroll: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
prepend: PropTypes.node,
|
||||
emptyMessage: PropTypes.node,
|
||||
};
|
||||
static defaultProps = {
|
||||
trackScroll: true,
|
||||
};
|
||||
state = {
|
||||
currentDetail: null,
|
||||
};
|
||||
|
||||
intersectionObserverWrapper = new IntersectionObserverWrapper();
|
||||
|
||||
handleScroll = throttle(() => {
|
||||
if (this.node) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = this.node;
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
this._oldScrollPosition = scrollHeight - scrollTop;
|
||||
|
||||
if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
|
||||
this.props.onScrollToBottom();
|
||||
} else if (scrollTop < 100 && this.props.onScrollToTop) {
|
||||
this.props.onScrollToTop();
|
||||
} else if (this.props.onScroll) {
|
||||
this.props.onScroll();
|
||||
}
|
||||
}
|
||||
}, 150, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
componentDidMount () {
|
||||
this.attachScrollListener();
|
||||
this.attachIntersectionObserver();
|
||||
|
||||
// Handle initial scroll posiiton
|
||||
this.handleScroll();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
// Reset the scroll position when a new toot comes in in order not to
|
||||
// jerk the scrollbar around if you're already scrolled down the page.
|
||||
if (prevProps.statusIds.size < this.props.statusIds.size && this._oldScrollPosition && this.node.scrollTop > 0) {
|
||||
if (prevProps.statusIds.first() !== this.props.statusIds.first()) {
|
||||
let newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
|
||||
if (this.node.scrollTop !== newScrollTop) {
|
||||
this.node.scrollTop = newScrollTop;
|
||||
}
|
||||
} else {
|
||||
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.detachScrollListener();
|
||||
this.detachIntersectionObserver();
|
||||
}
|
||||
|
||||
attachIntersectionObserver () {
|
||||
this.intersectionObserverWrapper.connect({
|
||||
root: this.node,
|
||||
rootMargin: '300% 0px',
|
||||
});
|
||||
}
|
||||
|
||||
detachIntersectionObserver () {
|
||||
this.intersectionObserverWrapper.disconnect();
|
||||
}
|
||||
|
||||
attachScrollListener () {
|
||||
this.node.addEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
|
||||
detachScrollListener () {
|
||||
this.node.removeEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
|
||||
setRef = (c) => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
handleLoadMore = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.onScrollToBottom();
|
||||
}
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
|
||||
const article = (() => {
|
||||
switch (e.key) {
|
||||
case 'PageDown':
|
||||
return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling;
|
||||
case 'PageUp':
|
||||
return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling;
|
||||
case 'End':
|
||||
return this.node.querySelector('[role="feed"] > article:last-of-type');
|
||||
case 'Home':
|
||||
return this.node.querySelector('[role="feed"] > article:first-of-type');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
if (article) {
|
||||
e.preventDefault();
|
||||
article.focus();
|
||||
article.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSetDetail = (id) => {
|
||||
this.setState({ currentDetail : id });
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
handleKeyDown,
|
||||
handleLoadMore,
|
||||
handleSetDetail,
|
||||
intersectionObserverWrapper,
|
||||
setRef,
|
||||
} = this;
|
||||
const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, intl } = this.props;
|
||||
const { currentDetail } = this.state;
|
||||
|
||||
const loadMore = (
|
||||
<CommonButton
|
||||
className='load-more'
|
||||
disabled={isLoading || statusIds.size > 0 && hasMore}
|
||||
onClick={handleLoadMore}
|
||||
showTitle
|
||||
title={intl.formatMessage(messages.load_more)}
|
||||
/>
|
||||
);
|
||||
let scrollableArea = null;
|
||||
|
||||
if (isLoading || statusIds.size > 0 || !emptyMessage) {
|
||||
scrollableArea = (
|
||||
<div className='scrollable' ref={setRef}>
|
||||
<div role='feed' className='status-list' onKeyDown={handleKeyDown}>
|
||||
{prepend}
|
||||
|
||||
{statusIds.map((statusId, index) => (
|
||||
<StatusContainer
|
||||
key={statusId}
|
||||
id={statusId}
|
||||
index={index}
|
||||
listLength={statusIds.size}
|
||||
detailed={currentDetail === statusId}
|
||||
setDetail={handleSetDetail}
|
||||
intersectionObserverWrapper={intersectionObserverWrapper}
|
||||
/>
|
||||
))}
|
||||
|
||||
{loadMore}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
scrollableArea = (
|
||||
<div className='empty-column-indicator' ref={setRef}>
|
||||
{emptyMessage}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (trackScroll) {
|
||||
return (
|
||||
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
|
||||
{scrollableArea}
|
||||
</ScrollContainer>
|
||||
);
|
||||
} else {
|
||||
return scrollableArea;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user