diff options
Diffstat (limited to 'app/javascript/flavours/glitch/containers')
12 files changed, 796 insertions, 0 deletions
diff --git a/app/javascript/flavours/glitch/containers/account_container.js b/app/javascript/flavours/glitch/containers/account_container.js new file mode 100644 index 000000000..bc84d299b --- /dev/null +++ b/app/javascript/flavours/glitch/containers/account_container.js @@ -0,0 +1,72 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { makeGetAccount } from 'flavours/glitch/selectors'; +import Account from 'flavours/glitch/components/account'; +import { + followAccount, + unfollowAccount, + blockAccount, + unblockAccount, + muteAccount, + unmuteAccount, +} from 'flavours/glitch/actions/accounts'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import { unfollowModal } from 'flavours/glitch/util/initial_state'; + +const messages = defineMessages({ + unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, +}); + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, props) => ({ + account: getAccount(state, props.id), + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { intl }) => ({ + + onFollow (account) { + if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { + if (unfollowModal) { + dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + })); + } else { + dispatch(unfollowAccount(account.get('id'))); + } + } else { + dispatch(followAccount(account.get('id'))); + } + }, + + onBlock (account) { + if (account.getIn(['relationship', 'blocking'])) { + dispatch(unblockAccount(account.get('id'))); + } else { + dispatch(blockAccount(account.get('id'))); + } + }, + + onMute (account) { + if (account.getIn(['relationship', 'muting'])) { + dispatch(unmuteAccount(account.get('id'))); + } else { + dispatch(initMuteModal(account)); + } + }, + + + onMuteNotifications (account, notifications) { + dispatch(muteAccount(account.get('id'), notifications)); + }, +}); + +export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account)); diff --git a/app/javascript/flavours/glitch/containers/compose_container.js b/app/javascript/flavours/glitch/containers/compose_container.js new file mode 100644 index 000000000..74c411b7c --- /dev/null +++ b/app/javascript/flavours/glitch/containers/compose_container.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import PropTypes from 'prop-types'; +import configureStore from 'flavours/glitch/store/configureStore'; +import { hydrateStore } from 'flavours/glitch/actions/store'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from 'mastodon/locales'; +import Compose from 'flavours/glitch/features/standalone/compose'; +import initialState from 'flavours/glitch/util/initial_state'; +import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +const store = configureStore(); + +if (initialState) { + store.dispatch(hydrateStore(initialState)); +} + +store.dispatch(fetchCustomEmojis()); + +export default class TimelineContainer extends React.PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + }; + + render () { + const { locale } = this.props; + + return ( + <IntlProvider locale={locale} messages={messages}> + <Provider store={store}> + <Compose /> + </Provider> + </IntlProvider> + ); + } + +} diff --git a/app/javascript/flavours/glitch/containers/domain_container.js b/app/javascript/flavours/glitch/containers/domain_container.js new file mode 100644 index 000000000..e92e102ab --- /dev/null +++ b/app/javascript/flavours/glitch/containers/domain_container.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { blockDomain, unblockDomain } from '../actions/domain_blocks'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Domain from '../components/domain'; +import { openModal } from '../actions/modal'; + +const messages = defineMessages({ + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' }, +}); + +const makeMapStateToProps = () => { + const mapStateToProps = (state, { }) => ({ + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { intl }) => ({ + onBlockDomain (domain) { + dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />, + confirm: intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => dispatch(blockDomain(domain)), + })); + }, + + onUnblockDomain (domain) { + dispatch(unblockDomain(domain)); + }, +}); + +export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Domain)); diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js new file mode 100644 index 000000000..1c0385b74 --- /dev/null +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -0,0 +1,32 @@ +import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu'; +import { openModal, closeModal } from 'flavours/glitch/actions/modal'; +import { connect } from 'react-redux'; +import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; +import { isUserTouching } from 'flavours/glitch/util/is_mobile'; + +const mapStateToProps = state => ({ + dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), + openDropdownId: state.getIn(['dropdown_menu', 'openId']), + openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), +}); + +const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({ + onOpen(id, onItemClick, dropdownPlacement, keyboard) { + dispatch(isUserTouching() ? openModal('ACTIONS', { + status, + actions: items.map( + (item, i) => item ? { + ...item, + name: `${item.text}-${i}`, + onClick: item.action ? ((e) => { return onItemClick(i, e) }) : null, + } : null + ), + }) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey)); + }, + onClose(id) { + dispatch(closeModal('ACTIONS')); + dispatch(closeDropdownMenu(id)); + }, +}); + +export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu); diff --git a/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js b/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js new file mode 100644 index 000000000..f2741f2d4 --- /dev/null +++ b/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import IntersectionObserverArticle from 'flavours/glitch/components/intersection_observer_article'; +import { setHeight } from 'flavours/glitch/actions/height_cache'; + +const makeMapStateToProps = (state, props) => ({ + cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]), +}); + +const mapDispatchToProps = (dispatch) => ({ + + onHeightChange (key, id, height) { + dispatch(setHeight(key, id, height)); + }, + +}); + +export default connect(makeMapStateToProps, mapDispatchToProps)(IntersectionObserverArticle); diff --git a/app/javascript/flavours/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js new file mode 100644 index 000000000..131303fd3 --- /dev/null +++ b/app/javascript/flavours/glitch/containers/mastodon.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import PropTypes from 'prop-types'; +import configureStore from 'flavours/glitch/store/configureStore'; +import { BrowserRouter, Route } from 'react-router-dom'; +import { ScrollContext } from 'react-router-scroll-4'; +import UI from 'flavours/glitch/features/ui'; +import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; +import { hydrateStore } from 'flavours/glitch/actions/store'; +import { connectUserStream } from 'flavours/glitch/actions/streaming'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from 'locales'; +import initialState from 'flavours/glitch/util/initial_state'; +import ErrorBoundary from 'flavours/glitch/components/error_boundary'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +export const store = configureStore(); +const hydrateAction = hydrateStore(initialState); +store.dispatch(hydrateAction); + +// load custom emojis +store.dispatch(fetchCustomEmojis()); + +export default class Mastodon extends React.PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + }; + + componentDidMount() { + this.disconnect = store.dispatch(connectUserStream()); + } + + componentWillUnmount () { + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; + } + } + + shouldUpdateScroll (_, { location }) { + return !(location.state?.mastodonModalKey); + } + + render () { + const { locale } = this.props; + + return ( + <IntlProvider locale={locale} messages={messages}> + <Provider store={store}> + <ErrorBoundary> + <BrowserRouter basename='/web'> + <ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}> + <Route path='/' component={UI} /> + </ScrollContext> + </BrowserRouter> + </ErrorBoundary> + </Provider> + </IntlProvider> + ); + } + +} diff --git a/app/javascript/flavours/glitch/containers/media_container.js b/app/javascript/flavours/glitch/containers/media_container.js new file mode 100644 index 000000000..8657b8064 --- /dev/null +++ b/app/javascript/flavours/glitch/containers/media_container.js @@ -0,0 +1,121 @@ +import React, { PureComponent, Fragment } from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { fromJS } from 'immutable'; +import { getLocale } from 'mastodon/locales'; +import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar'; +import MediaGallery from 'flavours/glitch/components/media_gallery'; +import Poll from 'flavours/glitch/components/poll'; +import Hashtag from 'flavours/glitch/components/hashtag'; +import ModalRoot from 'flavours/glitch/components/modal_root'; +import MediaModal from 'flavours/glitch/features/ui/components/media_modal'; +import Video from 'flavours/glitch/features/video'; +import Card from 'flavours/glitch/features/status/components/card'; +import Audio from 'flavours/glitch/features/audio'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; + +export default class MediaContainer extends PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + components: PropTypes.object.isRequired, + }; + + state = { + media: null, + index: null, + time: null, + backgroundColor: null, + options: null, + }; + + handleOpenMedia = (media, index) => { + document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; + + this.setState({ media, index }); + } + + handleOpenVideo = (options) => { + const { components } = this.props; + const { media } = JSON.parse(components[options.componetIndex].getAttribute('data-props')); + const mediaList = fromJS(media); + + document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; + + this.setState({ media: mediaList, options }); + } + + handleCloseMedia = () => { + document.body.classList.remove('with-modals--active'); + document.documentElement.style.marginRight = 0; + + this.setState({ + media: null, + index: null, + time: null, + backgroundColor: null, + options: null, + }); + } + + setBackgroundColor = color => { + this.setState({ backgroundColor: color }); + } + + render () { + const { locale, components } = this.props; + + return ( + <IntlProvider locale={locale} messages={messages}> + <Fragment> + {[].map.call(components, (component, i) => { + const componentName = component.getAttribute('data-component'); + const Component = MEDIA_COMPONENTS[componentName]; + const { media, card, poll, hashtag, ...props } = JSON.parse(component.getAttribute('data-props')); + + Object.assign(props, { + ...(media ? { media: fromJS(media) } : {}), + ...(card ? { card: fromJS(card) } : {}), + ...(poll ? { poll: fromJS(poll) } : {}), + ...(hashtag ? { hashtag: fromJS(hashtag) } : {}), + + ...(componentName === 'Video' ? { + componetIndex: i, + onOpenVideo: this.handleOpenVideo, + } : { + onOpenMedia: this.handleOpenMedia, + }), + }); + + return ReactDOM.createPortal( + <Component {...props} key={`media-${i}`} />, + component, + ); + })} + + <ModalRoot backgroundColor={this.state.backgroundColor} onClose={this.handleCloseMedia}> + {this.state.media && ( + <MediaModal + media={this.state.media} + index={this.state.index || 0} + currentTime={this.state.options?.startTime} + autoPlay={this.state.options?.autoPlay} + volume={this.state.options?.defaultVolume} + onClose={this.handleCloseMedia} + onChangeBackgroundColor={this.setBackgroundColor} + /> + )} + </ModalRoot> + </Fragment> + </IntlProvider> + ); + } + +} diff --git a/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js b/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js new file mode 100644 index 000000000..2570cf4a5 --- /dev/null +++ b/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js @@ -0,0 +1,49 @@ +// Package imports. +import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; + +// Our imports. +import NotificationPurgeButtons from 'flavours/glitch/components/notification_purge_buttons'; +import { + deleteMarkedNotifications, + enterNotificationClearingMode, + markAllNotifications, +} from 'flavours/glitch/actions/notifications'; +import { openModal } from 'flavours/glitch/actions/modal'; + +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)); diff --git a/app/javascript/flavours/glitch/containers/poll_container.js b/app/javascript/flavours/glitch/containers/poll_container.js new file mode 100644 index 000000000..345351cc6 --- /dev/null +++ b/app/javascript/flavours/glitch/containers/poll_container.js @@ -0,0 +1,25 @@ +import { connect } from 'react-redux'; +import { debounce } from 'lodash'; + +import Poll from 'flavours/glitch/components/poll'; +import { fetchPoll, vote } from 'flavours/glitch/actions/polls'; + +const mapDispatchToProps = (dispatch, { pollId }) => ({ + refresh: debounce( + () => { + dispatch(fetchPoll(pollId)); + }, + 1000, + { leading: true }, + ), + + onVote (choices) { + dispatch(vote(pollId, choices)); + }, +}); + +const mapStateToProps = (state, { pollId }) => ({ + poll: state.getIn(['polls', pollId]), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Poll); diff --git a/app/javascript/flavours/glitch/containers/scroll_container.js b/app/javascript/flavours/glitch/containers/scroll_container.js new file mode 100644 index 000000000..d21ff6368 --- /dev/null +++ b/app/javascript/flavours/glitch/containers/scroll_container.js @@ -0,0 +1,18 @@ +import { ScrollContainer as OriginalScrollContainer } from 'react-router-scroll-4'; + +// ScrollContainer is used to automatically scroll to the top when pushing a +// new history state and remembering the scroll position when going back. +// There are a few things we need to do differently, though. +const defaultShouldUpdateScroll = (prevRouterProps, { location }) => { + // If the change is caused by opening a modal, do not scroll to top + return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey); +}; + +export default +class ScrollContainer extends OriginalScrollContainer { + + static defaultProps = { + shouldUpdateScroll: defaultShouldUpdateScroll, + }; + +} diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js new file mode 100644 index 000000000..bc3c43d85 --- /dev/null +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -0,0 +1,261 @@ +import { connect } from 'react-redux'; +import Status from 'flavours/glitch/components/status'; +import { List as ImmutableList } from 'immutable'; +import { makeGetStatus, regexFromFilters, toServerSideType } from 'flavours/glitch/selectors'; +import { + replyCompose, + mentionCompose, + directCompose, +} from 'flavours/glitch/actions/compose'; +import { + reblog, + favourite, + bookmark, + unreblog, + unfavourite, + unbookmark, + pin, + unpin, +} from 'flavours/glitch/actions/interactions'; +import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses'; +import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import { initBlockModal } from 'flavours/glitch/actions/blocks'; +import { initReport } from 'flavours/glitch/actions/reports'; +import { initBoostModal } from 'flavours/glitch/actions/boosts'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { deployPictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; +import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state'; +import { filterEditLink } from 'flavours/glitch/util/backend_links'; +import { showAlertForError } from '../actions/alerts'; +import AccountContainer from 'flavours/glitch/containers/account_container'; +import Spoilers from '../components/spoilers'; +import Icon from 'flavours/glitch/components/icon'; + +const messages = defineMessages({ + deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, + deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, + redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, + redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, + replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, + replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' }, + author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' }, + matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' }, + editFilter: { id: 'confirmations.unfilter.edit_filter', defaultMessage: 'Edit filter' }, +}); + +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = (state, props) => { + + let status = getStatus(state, props); + let reblogStatus = status ? status.get('reblog', null) : null; + let account = undefined; + let prepend = undefined; + + if (props.featured && status) { + account = status.get('account'); + prepend = 'featured'; + } else if (reblogStatus !== null && typeof reblogStatus === 'object') { + account = status.get('account'); + status = reblogStatus; + prepend = 'reblogged_by'; + } + + return { + containerId : props.containerId || props.id, // Should match reblogStatus's id for reblogs + status : status, + account : account || props.account, + settings : state.get('local_settings'), + prepend : prepend || props.prepend, + usingPiP : state.get('picture_in_picture').statusId === props.id, + }; + }; + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ + + onReply (status, router) { + dispatch((_, getState) => { + let state = getState(); + + if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)), + onConfirm: () => dispatch(replyCompose(status, router)), + })); + } else { + dispatch(replyCompose(status, router)); + } + }); + }, + + onModalReblog (status, privacy) { + if (status.get('reblogged')) { + dispatch(unreblog(status)); + } else { + dispatch(reblog(status, privacy)); + } + }, + + onReblog (status, e) { + dispatch((_, getState) => { + let state = getState(); + if (state.getIn(['local_settings', 'confirm_boost_missing_media_description']) && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) { + dispatch(initBoostModal({ status, onReblog: this.onModalReblog, missingMediaDescription: true })); + } else if (e.shiftKey || !boostModal) { + this.onModalReblog(status); + } else { + dispatch(initBoostModal({ status, onReblog: this.onModalReblog })); + } + }); + }, + + onBookmark (status) { + if (status.get('bookmarked')) { + dispatch(unbookmark(status)); + } else { + dispatch(bookmark(status)); + } + }, + + onModalFavourite (status) { + dispatch(favourite(status)); + }, + + onFavourite (status, e) { + if (status.get('favourited')) { + dispatch(unfavourite(status)); + } else { + if (e.shiftKey || !favouriteModal) { + this.onModalFavourite(status); + } else { + dispatch(openModal('FAVOURITE', { status, onFavourite: this.onModalFavourite })); + } + } + }, + + onPin (status) { + if (status.get('pinned')) { + dispatch(unpin(status)); + } else { + dispatch(pin(status)); + } + }, + + onEmbed (status) { + dispatch(openModal('EMBED', { + url: status.get('url'), + onError: error => dispatch(showAlertForError(error)), + })); + }, + + onDelete (status, history, withRedraft = false) { + if (!deleteModal) { + dispatch(deleteStatus(status.get('id'), history, withRedraft)); + } else { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), + confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), + onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), + })); + } + }, + + onDirect (account, router) { + dispatch(directCompose(account, router)); + }, + + onMention (account, router) { + dispatch(mentionCompose(account, router)); + }, + + onOpenMedia (statusId, media, index) { + dispatch(openModal('MEDIA', { statusId, media, index })); + }, + + onOpenVideo (statusId, media, options) { + dispatch(openModal('VIDEO', { statusId, media, options })); + }, + + onBlock (status) { + const account = status.get('account'); + dispatch(initBlockModal(account)); + }, + + onUnfilter (status, onConfirm) { + dispatch((_, getState) => { + let state = getState(); + const serverSideType = toServerSideType(contextType); + const enabledFilters = state.get('filters', ImmutableList()).filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))).toArray(); + const searchIndex = status.get('search_index'); + const matchingFilters = enabledFilters.filter(filter => regexFromFilters([filter]).test(searchIndex)); + dispatch(openModal('CONFIRM', { + message: [ + <FormattedMessage id='confirmations.unfilter' defaultMessage='Information about this filtered toot' />, + <div className='filtered-status-info'> + <Spoilers spoilerText={intl.formatMessage(messages.author)}> + <AccountContainer id={status.getIn(['account', 'id'])} /> + </Spoilers> + <Spoilers spoilerText={intl.formatMessage(messages.matchingFilters, {count: matchingFilters.size})}> + <ul> + {matchingFilters.map(filter => ( + <li> + {filter.get('phrase')} + {!!filterEditLink && ' '} + {!!filterEditLink && ( + <a + target='_blank' + className='filtered-status-edit-link' + title={intl.formatMessage(messages.editFilter)} + href={filterEditLink(filter.get('id'))} + > + <Icon id='pencil' /> + </a> + )} + </li> + ))} + </ul> + </Spoilers> + </div> + ], + confirm: intl.formatMessage(messages.unfilterConfirm), + onConfirm: onConfirm, + })); + }); + }, + + onReport (status) { + dispatch(initReport(status.get('account'), status)); + }, + + onMute (account) { + dispatch(initMuteModal(account)); + }, + + onMuteConversation (status) { + if (status.get('muted')) { + dispatch(unmuteStatus(status.get('id'))); + } else { + dispatch(muteStatus(status.get('id'))); + } + }, + + deployPictureInPicture (status, type, mediaProps) { + dispatch((_, getState) => { + if (getState().getIn(['local_settings', 'media', 'pop_in_player'])) { + dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps)); + } + }); + }, + +}); + +export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/javascript/flavours/glitch/containers/timeline_container.js b/app/javascript/flavours/glitch/containers/timeline_container.js new file mode 100644 index 000000000..b61dc8dd7 --- /dev/null +++ b/app/javascript/flavours/glitch/containers/timeline_container.js @@ -0,0 +1,62 @@ +import React, { Fragment } from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import PropTypes from 'prop-types'; +import configureStore from 'flavours/glitch/store/configureStore'; +import { hydrateStore } from 'flavours/glitch/actions/store'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from 'mastodon/locales'; +import PublicTimeline from 'flavours/glitch/features/standalone/public_timeline'; +import HashtagTimeline from 'flavours/glitch/features/standalone/hashtag_timeline'; +import ModalContainer from 'flavours/glitch/features/ui/containers/modal_container'; +import initialState from 'flavours/glitch/util/initial_state'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +const store = configureStore(); + +if (initialState) { + store.dispatch(hydrateStore(initialState)); +} + +export default class TimelineContainer extends React.PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + hashtag: PropTypes.string, + local: PropTypes.bool, + }; + + static defaultProps = { + local: !initialState.settings.known_fediverse, + }; + + render () { + const { locale, hashtag, local } = this.props; + + let timeline; + + if (hashtag) { + timeline = <HashtagTimeline hashtag={hashtag} local={local} />; + } else { + timeline = <PublicTimeline local={local} />; + } + + return ( + <IntlProvider locale={locale} messages={messages}> + <Provider store={store}> + <Fragment> + {timeline} + + {ReactDOM.createPortal( + <ModalContainer />, + document.getElementById('modal-container'), + )} + </Fragment> + </Provider> + </IntlProvider> + ); + } + +} |