diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index c5efd46aa209d8286e1d49f34caacbcb5ef1fb53..c7afd4580f1bc65139033f49ffa553b78a44bc69 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -40,7 +40,7 @@ class Api::V1::Timelines::TagController < Api::BaseController any: params[:any], all: params[:all], none: params[:none], - local: false, + local: truthy_param?(:local), remote: truthy_param?(:remote), only_media: truthy_param?(:only_media) ) @@ -51,7 +51,7 @@ class Api::V1::Timelines::TagController < Api::BaseController end def pagination_params(core_params) - params.slice(:limit, :only_media).permit(:limit, :only_media).merge(core_params) + params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params) end def next_path diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index f75cd5ff4a546a5b7e91308e9f7d7d734d19e23c..6616ba107c83f3d4836582b68ecf3940ac1d4e70 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -43,7 +43,7 @@ class TagsController < ApplicationController end def set_local - @local = false + @local = truthy_param?(:local) end def set_statuses diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index e8983df867b6fe76c4a72de3f88822341b4a0098..7a5a719bdbb86795ceb387c2ef05dabea52506c9 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -198,6 +198,7 @@ export function submitCompose(routerHistory) { } if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { + insertIfOnline('community'); insertIfOnline('public'); insertIfOnline(`account:${response.data.account.id}`); } diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 10f5be5e8dbb3b83924034a50e96af77f335ce6e..b8e07d5dc42521c622ed6f09557b750f853ef18c 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -117,6 +117,14 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => { export const connectUserStream = () => connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification }); +/** + * @param {Object} options + * @param {boolean} [options.onlyMedia] + * @return {function(): void} + */ +export const connectCommunityStream = ({ onlyMedia } = {}) => + connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); + /** * @param {string} domain * @param {Object} options @@ -148,11 +156,12 @@ export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => /** * @param {string} columnId * @param {string} tagName + * @param {boolean} onlyLocal * @param {function(object): boolean} accept * @return {function(): void} */ -export const connectHashtagStream = (columnId, tagName, accept) => - connectTimelineStream(`hashtag:${columnId}`, `hashtag`, { tag: tagName }, { accept }); +export const connectHashtagStream = (columnId, tagName, onlyLocal, accept) => + connectTimelineStream(`hashtag:${columnId}${onlyLocal ? ':local' : ''}`, `hashtag${onlyLocal ? ':local' : ''}`, { tag: tagName }, { accept }); /** * @return {function(): void} diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 1e678a52778335e348db6d3326f2847ad3023bb6..376987bad66349253925dbbd3daee99a21ee35aa 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -121,18 +121,20 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done); +export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandDomainTimeline = (domain, { maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`domain${onlyMedia ? ':media' : ''}:${domain}`, '/api/v1/timelines/public', { local: false, domain: domain, max_id: maxId, only_media: !!onlyMedia }, done); export const expandGroupTimeline = (id, { maxId, onlyMedia, tagged } = {}, done = noOp) => expandTimeline(`group:${id}${onlyMedia ? ':media' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: !!onlyMedia, tagged: tagged }, done); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); -export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => { - return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { +export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { + return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId, any: parseTags(tags, 'any'), all: parseTags(tags, 'all'), none: parseTags(tags, 'none'), + local: local, }, done); }; diff --git a/app/javascript/mastodon/features/community_timeline/components/column_settings.js b/app/javascript/mastodon/features/community_timeline/components/column_settings.js new file mode 100644 index 0000000000000000000000000000000000000000..0cb6db8836b9bda09fcd78e3f89c80b789170322 --- /dev/null +++ b/app/javascript/mastodon/features/community_timeline/components/column_settings.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import SettingToggle from '../../notifications/components/setting_toggle'; + +export default @injectIntl +class ColumnSettings extends React.PureComponent { + + static propTypes = { + settings: ImmutablePropTypes.map.isRequired, + onChange: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + columnId: PropTypes.string, + }; + + render () { + const { settings, onChange } = this.props; + + return ( + <div> + <div className='column-settings__row'> + <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} /> + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/community_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/community_timeline/containers/column_settings_container.js new file mode 100644 index 0000000000000000000000000000000000000000..405064c3fcba30c1b26fb2c923d8be035f6dfcf7 --- /dev/null +++ b/app/javascript/mastodon/features/community_timeline/containers/column_settings_container.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux'; +import ColumnSettings from '../components/column_settings'; +import { changeSetting } from '../../../actions/settings'; +import { changeColumnParams } from '../../../actions/columns'; + +const mapStateToProps = (state, { columnId }) => { + const uuid = columnId; + const columns = state.getIn(['settings', 'columns']); + const index = columns.findIndex(c => c.get('uuid') === uuid); + + return { + settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'community']), + }; +}; + +const mapDispatchToProps = (dispatch, { columnId }) => { + return { + onChange (key, checked) { + if (columnId) { + dispatch(changeColumnParams(columnId, key, checked)); + } else { + dispatch(changeSetting(['community', ...key], checked)); + } + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b3cd39685cd79080b2994011bf93b9b84c6a811d --- /dev/null +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -0,0 +1,137 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; +import StatusListContainer from '../ui/containers/status_list_container'; +import Column from '../../components/column'; +import ColumnHeader from '../../components/column_header'; +import { expandCommunityTimeline } from '../../actions/timelines'; +import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; +import ColumnSettingsContainer from './containers/column_settings_container'; +import { connectCommunityStream } from '../../actions/streaming'; + +const messages = defineMessages({ + title: { id: 'column.community', defaultMessage: 'Local timeline' }, +}); + +const mapStateToProps = (state, { columnId }) => { + const uuid = columnId; + const columns = state.getIn(['settings', 'columns']); + const index = columns.findIndex(c => c.get('uuid') === uuid); + const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']); + const timelineState = state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`]); + + return { + hasUnread: !!timelineState && timelineState.get('unread') > 0, + onlyMedia, + }; +}; + +export default @connect(mapStateToProps) +@injectIntl +class CommunityTimeline extends React.PureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static defaultProps = { + onlyMedia: false, + }; + + static propTypes = { + dispatch: PropTypes.func.isRequired, + shouldUpdateScroll: PropTypes.func, + columnId: PropTypes.string, + intl: PropTypes.object.isRequired, + hasUnread: PropTypes.bool, + multiColumn: PropTypes.bool, + onlyMedia: PropTypes.bool, + }; + + handlePin = () => { + const { columnId, dispatch, onlyMedia } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('COMMUNITY', { other: { onlyMedia } })); + } + } + + handleMove = (dir) => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + } + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + componentDidMount () { + const { dispatch, onlyMedia } = this.props; + + dispatch(expandCommunityTimeline({ onlyMedia })); + this.disconnect = dispatch(connectCommunityStream({ onlyMedia })); + } + + componentDidUpdate (prevProps) { + if (prevProps.onlyMedia !== this.props.onlyMedia) { + const { dispatch, onlyMedia } = this.props; + + this.disconnect(); + dispatch(expandCommunityTimeline({ onlyMedia })); + this.disconnect = dispatch(connectCommunityStream({ onlyMedia })); + } + } + + componentWillUnmount () { + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; + } + } + + setRef = c => { + this.column = c; + } + + handleLoadMore = maxId => { + const { dispatch, onlyMedia } = this.props; + + dispatch(expandCommunityTimeline({ maxId, onlyMedia })); + } + + render () { + const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn, onlyMedia } = this.props; + const pinned = !!columnId; + + return ( + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> + <ColumnHeader + icon='users' + active={hasUnread} + title={intl.formatMessage(messages.title)} + onPin={this.handlePin} + onMove={this.handleMove} + onClick={this.handleHeaderClick} + pinned={pinned} + multiColumn={multiColumn} + > + <ColumnSettingsContainer columnId={columnId} /> + </ColumnHeader> + + <StatusListContainer + trackScroll={!pinned} + scrollKey={`community_timeline-${columnId}`} + timelineId={`community${onlyMedia ? ':media' : ''}`} + onLoadMore={this.handleLoadMore} + emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} + shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} + /> + </Column> + ); + } + +} diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 09f63143649229878629110548bc35d716d4c46e..5953d9647db045b332c2b3bd5127fa4fe04764f7 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -23,7 +23,7 @@ const messages = defineMessages({ home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, - lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, + community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' }, @@ -104,6 +104,9 @@ class Compose extends React.PureComponent { {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link> )} + {!columns.some(column => column.get('id') === 'COMMUNITY') && ( + <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link> + )} {!columns.some(column => column.get('id') === 'PUBLIC') && ( <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link> )} diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 1f07e554b0b0042b9bbe095ca4d20de7b3d6e873..b0968bc102275cec9025fe1c0a7d28062f7dcee7 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -20,6 +20,7 @@ const messages = defineMessages({ notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' }, + community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' }, bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, @@ -102,10 +103,11 @@ class GettingStarted extends ImmutablePureComponent { if (multiColumn) { navItems.push( <ColumnSubheading key='header-discover' text={intl.formatMessage(messages.discover)} />, + <ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />, <ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />, ); - height += 34 + 48; + height += 34 + 48*2; navItems.push( <ColumnLink key='group_directory' icon='address-book' text={intl.formatMessage(messages.group_directory)} to='/group_directory' />, diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js index 292c5cc6f5dc46043a33863de7643de11efeb0f0..27300f020df3ba54a5a842bfef5c6c02b0d679e6 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js +++ b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js @@ -5,6 +5,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Toggle from 'react-toggle'; import AsyncSelect from 'react-select/async'; import { NonceProvider } from 'react-select'; +import SettingToggle from '../../notifications/components/setting_toggle'; const messages = defineMessages({ placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' }, @@ -111,6 +112,10 @@ class ColumnSettings extends React.PureComponent { {this.modeSelect('none')} </div> )} + + <div className='column-settings__row'> + <SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} /> + </div> </div> ); } diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js index 9cc34b4e870b36581a73691d38c96218c22ccb41..5ccd9f8ea2ecd3c6a89dd6b0b8c3473d84a81906 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/hashtag_timeline/index.js @@ -12,7 +12,7 @@ import { connectHashtagStream } from '../../actions/streaming'; import { isEqual } from 'lodash'; const mapStateToProps = (state, props) => ({ - hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0, + hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0, }); export default @connect(mapStateToProps) @@ -76,13 +76,13 @@ class HashtagTimeline extends React.PureComponent { this.column.scrollTop(); } - _subscribe (dispatch, id, tags = {}) { + _subscribe (dispatch, id, tags = {}, local) { let any = (tags.any || []).map(tag => tag.value); let all = (tags.all || []).map(tag => tag.value); let none = (tags.none || []).map(tag => tag.value); [id, ...any].map(tag => { - this.disconnects.push(dispatch(connectHashtagStream(id, tag, status => { + this.disconnects.push(dispatch(connectHashtagStream(id, tag, local, status => { let tags = status.tags.map(tag => tag.name); return all.filter(tag => tags.includes(tag)).length === all.length && @@ -98,21 +98,21 @@ class HashtagTimeline extends React.PureComponent { componentDidMount () { const { dispatch } = this.props; - const { id, tags } = this.props.params; + const { id, tags, local } = this.props.params; - this._subscribe(dispatch, id, tags); - dispatch(expandHashtagTimeline(id, { tags })); + this._subscribe(dispatch, id, tags, local); + dispatch(expandHashtagTimeline(id, { tags, local })); } componentWillReceiveProps (nextProps) { const { dispatch, params } = this.props; - const { id, tags } = nextProps.params; + const { id, tags, local } = nextProps.params; - if (id !== params.id || !isEqual(tags, params.tags)) { + if (id !== params.id || !isEqual(tags, params.tags) || !isEqual(local, params.local)) { this._unsubscribe(); - this._subscribe(dispatch, id, tags); - dispatch(clearTimeline(`hashtag:${id}`)); - dispatch(expandHashtagTimeline(id, { tags })); + this._subscribe(dispatch, id, tags, local); + dispatch(clearTimeline(`hashtag:${id}${local ? ':local' : ''}`)); + dispatch(expandHashtagTimeline(id, { tags, local })); } } @@ -125,13 +125,13 @@ class HashtagTimeline extends React.PureComponent { } handleLoadMore = maxId => { - const { id, tags } = this.props.params; - this.props.dispatch(expandHashtagTimeline(id, { maxId, tags })); + const { id, tags, local } = this.props.params; + this.props.dispatch(expandHashtagTimeline(id, { maxId, tags, local })); } render () { const { shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props; - const { id } = this.props.params; + const { id, local } = this.props.params; const pinned = !!columnId; return ( @@ -153,7 +153,7 @@ class HashtagTimeline extends React.PureComponent { <StatusListContainer trackScroll={!pinned} scrollKey={`hashtag_timeline-${columnId}`} - timelineId={`hashtag:${id}`} + timelineId={`hashtag:${id}${local ? ':local' : ''}`} onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} shouldUpdateScroll={shouldUpdateScroll} diff --git a/app/javascript/mastodon/features/introduction/index.js b/app/javascript/mastodon/features/introduction/index.js index 23b7adeb9b49680ef7be37d4c42ea479800e1796..5820750a4ee0bd6427719f9fb316eecb42c9e19f 100644 --- a/app/javascript/mastodon/features/introduction/index.js +++ b/app/javascript/mastodon/features/introduction/index.js @@ -45,6 +45,11 @@ const FrameFederation = ({ onNext }) => ( <p><FormattedMessage id='introduction.federation.home.text' defaultMessage='Posts from people you follow will appear in your home feed. You can follow anyone on any server!' /></p> </div> + <div> + <h3><FormattedMessage id='introduction.federation.local.headline' defaultMessage='Local' /></h3> + <p><FormattedMessage id='introduction.federation.local.text' defaultMessage='Public posts from people on the same server as you will appear in the local timeline.' /></p> + </div> + <div> <h3><FormattedMessage id='introduction.federation.federated.headline' defaultMessage='Federated' /></h3> <p><FormattedMessage id='introduction.federation.federated.text' defaultMessage='Public posts from other servers of the fediverse will appear in the federated timeline.' /></p> diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.js b/app/javascript/mastodon/features/keyboard_shortcuts/index.js index 03b599dc0af772ffc8261a3487600bfc2805fa4d..d278d2b264b7d3ceb41c3e9eddd30a5f69162613 100644 --- a/app/javascript/mastodon/features/keyboard_shortcuts/index.js +++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.js @@ -112,6 +112,10 @@ class KeyboardShortcuts extends ImmutablePureComponent { <td><kbd>g</kbd>+<kbd>n</kbd></td> <td><FormattedMessage id='keyboard_shortcuts.notifications' defaultMessage='to open notifications column' /></td> </tr> + <tr> + <td><kbd>g</kbd>+<kbd>l</kbd></td> + <td><FormattedMessage id='keyboard_shortcuts.local' defaultMessage='to open local timeline' /></td> + </tr> <tr> <td><kbd>g</kbd>+<kbd>t</kbd></td> <td><FormattedMessage id='keyboard_shortcuts.federated' defaultMessage='to open federated timeline' /></td> diff --git a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js index b8cd5b6a403e66f5f82491cc08724a94adcfa841..d3d8a6507e6d19701386a3e20b4dacae89f603dc 100644 --- a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js @@ -24,20 +24,25 @@ class HashtagTimeline extends React.PureComponent { isLoading: PropTypes.bool.isRequired, hasMore: PropTypes.bool.isRequired, hashtag: PropTypes.string.isRequired, + local: PropTypes.bool.isRequired, + }; + + static defaultProps = { + local: false, }; componentDidMount () { - const { dispatch, hashtag } = this.props; + const { dispatch, hashtag, local } = this.props; - dispatch(expandHashtagTimeline(hashtag)); + dispatch(expandHashtagTimeline(hashtag, { local })); } handleLoadMore = () => { - const { dispatch, hashtag, statusIds } = this.props; + const { dispatch, hashtag, local, statusIds } = this.props; const maxId = statusIds.last(); if (maxId) { - dispatch(expandHashtagTimeline(hashtag, { maxId })); + dispatch(expandHashtagTimeline(hashtag, { maxId, local })); } } diff --git a/app/javascript/mastodon/features/standalone/public_timeline/index.js b/app/javascript/mastodon/features/standalone/public_timeline/index.js index 78f08ce3aa36ae77a217ad35be3cbceb252024e9..19b0b14be6a35d2decfb9f4f89825672b397f222 100644 --- a/app/javascript/mastodon/features/standalone/public_timeline/index.js +++ b/app/javascript/mastodon/features/standalone/public_timeline/index.js @@ -2,15 +2,15 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { expandPublicTimeline } from 'mastodon/actions/timelines'; +import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines'; import Masonry from 'react-masonry-infinite'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import DetailedStatusContainer from 'mastodon/features/status/containers/detailed_status_container'; import { debounce } from 'lodash'; import LoadingIndicator from 'mastodon/components/loading_indicator'; -const mapStateToProps = (state) => { - const timeline = state.getIn(['timelines', 'public'], ImmutableMap()); +const mapStateToProps = (state, { local }) => { + const timeline = state.getIn(['timelines', local ? 'community' : 'public'], ImmutableMap()); return { statusIds: timeline.get('items', ImmutableList()), @@ -27,6 +27,7 @@ class PublicTimeline extends React.PureComponent { statusIds: ImmutablePropTypes.list.isRequired, isLoading: PropTypes.bool.isRequired, hasMore: PropTypes.bool.isRequired, + local: PropTypes.bool, }; componentDidMount () { @@ -34,20 +35,23 @@ class PublicTimeline extends React.PureComponent { } componentDidUpdate (prevProps) { + if (prevProps.local !== this.props.local) { + this._connect(); + } } _connect () { - const { dispatch } = this.props; + const { dispatch, local } = this.props; - dispatch(expandPublicTimeline()); + dispatch(local ? expandCommunityTimeline() : expandPublicTimeline()); } handleLoadMore = () => { - const { dispatch, statusIds } = this.props; + const { dispatch, statusIds, local } = this.props; const maxId = statusIds.last(); if (maxId) { - dispatch(expandPublicTimeline({ maxId })); + dispatch(local ? expandCommunityTimeline({ maxId }) : expandPublicTimeline({ maxId })); } } diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 08ef1d911cb33a8fa5e284d4baa80640617e4a67..f2f71362e52a6e2314f2a3724aa7009ae9aac7b2 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -19,6 +19,7 @@ import { Notifications, HomeTimeline, GroupTimeline, + CommunityTimeline, PublicTimeline, DomainTimeline, HashtagTimeline, @@ -43,6 +44,7 @@ const componentMap = { 'NOTIFICATIONS': Notifications, 'PUBLIC': PublicTimeline, 'REMOTE': PublicTimeline, + 'COMMUNITY': CommunityTimeline, 'DOMAIN': DomainTimeline, 'GROUP': GroupTimeline, 'HASHTAG': HashtagTimeline, diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index 1f0c2cf27101d9fa41e363f81138668bccad6dcc..3df724bb48d6ca77ba52a7f68c95e765ede6eb7f 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.js +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js @@ -15,6 +15,7 @@ const NavigationPanel = () => ( <NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink> <NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink> <FollowRequestsNavLink /> + <NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink> <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> <NavLink className='column-link column-link--transparent' to='/accounts/95378'><Icon className='column-link__icon' id='info-circle' fixedWidth /><FormattedMessage id='navigation_bar.information_acct' defaultMessage='QOTO info' /></NavLink> <NavLink className='column-link column-link--transparent' to='/timelines/tag/QOTOJournal'><Icon className='column-link__icon' id='hashtag' fixedWidth /><FormattedMessage id='navigation_bar.hashtag_qoto_journal' defaultMessage='QOTO Journal' /></NavLink> diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index e5ad19949963b957e2381c1a04e523016a362dce..1e53dd8aa0faa46e5b72790dd15627fd72824d2c 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -10,6 +10,7 @@ import NotificationsCounterIcon from './notifications_counter_icon'; export const links = [ <NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, <NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, + <NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, <NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, <NavLink className='tabs-bar__link' exact to='/lists' data-preview-title-id='column.lists' data-preview-icon='list-ul' ><Icon id='list-ul' fixedWidth /><FormattedMessage id='tabs_bar.lists' defaultMessage='Lists' /></NavLink>, <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>, diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 3053f78c497f3d391fa38deb1e3d60fff8ed35de..4fc2ea78e27dc6a316224ab86df80e83bcf5c2fb 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -28,6 +28,7 @@ import { GettingStarted, KeyboardShortcuts, PublicTimeline, + CommunityTimeline, DomainTimeline, GroupTimeline, AccountTimeline, @@ -95,6 +96,7 @@ const keyMap = { back: 'backspace', goToHome: 'g h', goToNotifications: 'g n', + goToLocal: 'g l', goToFederated: 'g t', goToDirect: 'g d', goToStart: 'g s', @@ -191,6 +193,7 @@ class SwitchingColumnsArea extends React.PureComponent { <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> + <WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/timelines/public/domain/:domain' exact component={DomainTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/timelines/groups/:id/:tagged?' exact component={GroupTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> @@ -480,6 +483,10 @@ class UI extends React.PureComponent { this.context.router.history.push('/notifications'); } + handleHotkeyGoToLocal = () => { + this.context.router.history.push('/timelines/public/local'); + } + handleHotkeyGoToFederated = () => { this.context.router.history.push('/timelines/public'); } @@ -530,6 +537,7 @@ class UI extends React.PureComponent { back: this.handleHotkeyBack, goToHome: this.handleHotkeyGoToHome, goToNotifications: this.handleHotkeyGoToNotifications, + goToLocal: this.handleHotkeyGoToLocal, goToFederated: this.handleHotkeyGoToFederated, goToDirect: this.handleHotkeyGoToDirect, goToStart: this.handleHotkeyGoToStart, diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 1732c323159f199025a7f7eb3ae033dd4f2a7bb4..4df47fdbc6a3cf6b5d3000e21597bec5074366b9 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -18,6 +18,10 @@ export function PublicTimeline () { return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline'); } +export function CommunityTimeline () { + return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline'); +} + export function DomainTimeline () { return import(/* webpackChunkName: "features/domain_timeline" */'../../domain_timeline'); } diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index e49837fc8aa3ce9cda6638e5685a8e808b80b76e..2c83357aa8fef2b6313d520a42cde46806be9678 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -245,7 +245,7 @@ "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", "introduction.federation.action": "Next", "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Public posts from various Fediverse servers are displayed on the federation timeline.", + "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", "introduction.federation.home.headline": "Home", "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", "introduction.federation.local.headline": "Local", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 2488cd9788ff8e79c1087f0b700443f6f6b9b384..e95fbac1b1bba5feb1bd792f2d80c38da8acf572 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -241,7 +241,7 @@ "intervals.full.minutes": "{number}分", "introduction.federation.action": "次ã¸", "introduction.federation.federated.headline": "連åˆã‚¿ã‚¤ãƒ ライン", - "introduction.federation.federated.text": "Fediverseã®æ§˜ã€…ãªã‚µãƒ¼ãƒãƒ¼ã‹ã‚‰ã®å…¬é–‹æŠ•稿ãŒé€£åˆã‚¿ã‚¤ãƒ ラインã«è¡¨ç¤ºã•れã¾ã™ã€‚", + "introduction.federation.federated.text": "Fediverseã®ä»–ã®ã‚µãƒ¼ãƒãƒ¼ã‹ã‚‰ã®å…¬é–‹æŠ•稿ã¯é€£åˆã‚¿ã‚¤ãƒ ラインã«è¡¨ç¤ºã•れã¾ã™ã€‚", "introduction.federation.home.headline": "ホームタイムライン", "introduction.federation.home.text": "フォãƒãƒ¼ã—ã¦ã„ã‚‹äººã€…ã®æŠ•ç¨¿ã¯ãƒ›ãƒ¼ãƒ タイムラインã«è¡¨ç¤ºã•れã¾ã™ã€‚ã©ã“ã®ã‚µãƒ¼ãƒãƒ¼ã®èª°ã§ã‚‚フォãƒãƒ¼ã§ãã¾ã™ï¼", "introduction.federation.local.headline": "ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ライン", diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 7abc4064eb111b59c497641adf9b0bde97394f8e..c8c7ba39055baa22539a3ba6f3cab165a1071c18 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -65,6 +65,12 @@ const initialState = ImmutableMap({ }), }), + community: ImmutableMap({ + regex: ImmutableMap({ + body: '', + }), + }), + domain: ImmutableMap({ regex: ImmutableMap({ body: '', diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js index 2d92e73d6e4fe904d648dfb4486baba4049bafc3..19f63451eb85eac409a571ba47d044afe66c2904 100644 --- a/app/javascript/mastodon/stream.js +++ b/app/javascript/mastodon/stream.js @@ -99,7 +99,7 @@ const sharedCallbacks = { const streamIdentifier = stream[1]; - if (channelName === 'hashtag') { + if (['hashtag', 'hashtag:local'].includes(channelName)) { return channelName === streamChannelName && params.tag === streamIdentifier; } else if (channelName === 'list') { return channelName === streamChannelName && params.list === streamIdentifier; diff --git a/app/models/public_feed.rb b/app/models/public_feed.rb index 403ee071c9014ca92af96d0e960c290b8ba2e9d7..c6d0b37a93e1a30e56c48e88a04bbb827be460a9 100644 --- a/app/models/public_feed.rb +++ b/app/models/public_feed.rb @@ -19,13 +19,12 @@ class PublicFeed < Feed # @param [Integer] min_id # @return [Array<Status>] def get(limit, max_id = nil, since_id = nil, min_id = nil) - return Status.none if local_only? && !imast? - scope = public_scope scope.merge!(without_replies_scope) unless with_replies? scope.merge!(without_reblogs_scope) unless with_reblogs? scope.merge!(remote_only_scope) if remote_only? + scope.merge!(local_only_scope) if local_only? scope.merge!(domain_only_scope) if domain_only? scope.merge!(account_filters_scope) if account? scope.merge!(media_only_scope) if media_only? diff --git a/app/models/tag_feed.rb b/app/models/tag_feed.rb index 0da6606de6e8b9a6abf43a017f34637993e07eda..9a16ffc82b312123a1effa1b56f7f87db3845672 100644 --- a/app/models/tag_feed.rb +++ b/app/models/tag_feed.rb @@ -29,6 +29,7 @@ class TagFeed < PublicFeed scope.merge!(tagged_with_any_scope) scope.merge!(tagged_with_all_scope) scope.merge!(tagged_with_none_scope) + scope.merge!(local_only_scope) if local_only? scope.merge!(remote_only_scope) if remote_only? scope.merge!(account_filters_scope) if account? scope.merge!(media_only_scope) if media_only? diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index cc1988f48b13c3c3362b2e84c85957cde9c2c6f8..406f25feab472095e8a270027ec9a46d4d687f9d 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -96,6 +96,7 @@ class BatchedRemoveStatusService < BaseService redis.pipelined do redis.publish('timeline:public', payload) if status.local? + redis.publish('timeline:public:local', payload) else redis.publish('timeline:public:remote', payload) redis.publish("timeline:public:domain:#{@domains[status.id].mb_chars.downcase}", payload) @@ -104,6 +105,7 @@ class BatchedRemoveStatusService < BaseService if status.media_attachments.any? redis.publish('timeline:public:media', payload) if status.local? + redis.publish('timeline:public:local:media', payload) else redis.publish('timeline:public:remote:media', payload) redis.publish("timeline:public:domain:media:#{@domains[status.id].mb_chars.downcase}", payload) @@ -112,6 +114,7 @@ class BatchedRemoveStatusService < BaseService @tags[status.id].each do |hashtag| redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", payload) + redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", payload) if status.local? end end end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 332c2ed61185658945d6707ba2c240f78c3ad166..3e48e0440be133599a9613244e1f3ed766050cd2 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -179,6 +179,7 @@ class FanOutOnWriteService < BaseService status.tags.pluck(:name).each do |hashtag| Redis.current.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload) + Redis.current.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if status.local? end end @@ -226,6 +227,7 @@ class FanOutOnWriteService < BaseService Redis.current.publish('timeline:public', @payload) if status.local? + Redis.current.publish('timeline:public:local', @payload) else Redis.current.publish('timeline:public:remote', @payload) Redis.current.publish("timeline:public:domain:#{status.account.domain.mb_chars.downcase}", @payload) @@ -237,6 +239,7 @@ class FanOutOnWriteService < BaseService Redis.current.publish('timeline:public:media', @payload) if status.local? + Redis.current.publish('timeline:public:local:media', @payload) else Redis.current.publish('timeline:public:remote:media', @payload) Redis.current.publish("timeline:public:domain:media:#{status.account.domain.mb_chars.downcase}", @payload) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 1982b442b23f2593cad3ad3a637b383f9c658cb5..bb67162ad7d1f11b06e05b6b4f841857a46cb0fa 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -133,6 +133,7 @@ class RemoveStatusService < BaseService @tags.each do |hashtag| redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload) + redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if @status.local? end end @@ -159,6 +160,7 @@ class RemoveStatusService < BaseService redis.publish('timeline:public', @payload) if @status.local? + redis.publish('timeline:public:local', @payload) else redis.publish('timeline:public:remote', @payload) redis.publish("timeline:public:domain:#{@account.domain.mb_chars.downcase}", @payload) @@ -170,6 +172,7 @@ class RemoveStatusService < BaseService redis.publish('timeline:public:media', @payload) if @status.local? + redis.publish('timeline:public:local:media', @payload) else redis.publish('timeline:public:remote:media', @payload) redis.publish("timeline:public:domain:media:#{@account.domain.mb_chars.downcase}", @payload) diff --git a/app/views/user_mailer/welcome.html.haml b/app/views/user_mailer/welcome.html.haml index 6f5cfb6932644d95ff4594dcaaf8990a34ff62a1..1f75ff48ae453457002a0df887749e569d00ff66 100644 --- a/app/views/user_mailer/welcome.html.haml +++ b/app/views/user_mailer/welcome.html.haml @@ -138,5 +138,7 @@ %span= t 'user_mailer.welcome.tip_mobile_webapp' %li %span= t 'user_mailer.welcome.tip_following' + %li + %span= t 'user_mailer.welcome.tip_local_timeline', instance: @instance %li %span= t 'user_mailer.welcome.tip_federated_timeline' diff --git a/app/views/user_mailer/welcome.text.erb b/app/views/user_mailer/welcome.text.erb index 1bf7fbf450d2e83156365078be034ce5247a6d88..e310d7ca6f963a1e6e431b847defbe5fdb5d1c0a 100644 --- a/app/views/user_mailer/welcome.text.erb +++ b/app/views/user_mailer/welcome.text.erb @@ -25,4 +25,5 @@ * <%= t 'user_mailer.welcome.tip_mobile_webapp' %> * <%= t 'user_mailer.welcome.tip_following' %> +* <%= t 'user_mailer.welcome.tip_local_timeline', instance: @instance %> * <%= t 'user_mailer.welcome.tip_federated_timeline' %> diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 3d2e1749f692a7c44322a074f3eb2d17c1cd241b..756236739a85a59a036965937fb34dee8c10b742 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1392,14 +1392,14 @@ ja: edit_profile_step: アイコンやヘッダーã®ç”»åƒã‚’アップãƒãƒ¼ãƒ‰ã—ãŸã‚Šã€è¡¨ç¤ºåを変更ã—ãŸã‚Šã—ã¦ã€è‡ªåˆ†ã®ãƒ—ãƒãƒ•ィールをカスタマイズã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ã¾ãŸã€èª°ã‹ã‹ã‚‰ã®æ–°è¦ãƒ•ã‚©ãƒãƒ¼ã‚’許å¯ã™ã‚‹å‰ã«ãã®äººã®æ§˜åを見ã¦ãŠããŸã„å ´åˆã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’承èªåˆ¶ã«ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚ explanation: å§‹ã‚ã‚‹ã«ã‚ãŸã£ã¦ã®ã‚¢ãƒ‰ãƒã‚¤ã‚¹ã§ã™ final_action: å§‹ã‚ã¾ã—ょㆠ- final_step: 'ã•ã‚ã€å§‹ã‚ã¾ã—ょã†ï¼ ãŸã¨ãˆãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒã¾ã ã„ãªãã¦ã‚‚ã€ã‚ãªãŸã®å…¬é–‹ã—ãŸæŠ•ç¨¿ã¯é€£åˆã‚¿ã‚¤ãƒ ラインやãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ãªã©ã‚’通ã˜ã¦èª°ã‹ã®ç›®ã«ã¨ã¾ã‚‹ã¯ãšã§ã™ã€‚自己紹介をã—ãŸã„ã¨ãã«ã¯ #QOTO ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ãŒä¾¿åˆ©ã‹ã‚‚ã—れã¾ã›ã‚“。' + final_step: 'ã•ã‚ã€å§‹ã‚ã¾ã—ょã†ï¼ ãŸã¨ãˆãƒ•ã‚©ãƒãƒ¯ãƒ¼ãŒã¾ã ã„ãªãã¦ã‚‚ã€ã‚ãªãŸã®å…¬é–‹ã—ãŸæŠ•ç¨¿ã¯ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインやãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ãªã©ã‚’通ã˜ã¦èª°ã‹ã®ç›®ã«ã¨ã¾ã‚‹ã¯ãšã§ã™ã€‚自己紹介をã—ãŸã„ã¨ãã«ã¯ #introductions ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ãŒä¾¿åˆ©ã‹ã‚‚ã—れã¾ã›ã‚“。' full_handle: ã‚ãªãŸã®æ£å¼ãªãƒ¦ãƒ¼ã‚¶ãƒ¼ID full_handle_hint: 別ã®ã‚µãƒ¼ãƒãƒ¼ã®å‹é”ã¨ãƒ•ã‚©ãƒãƒ¼ã‚„メッセージをやりå–りã™ã‚‹éš›ã«ã¯ã€ã“れをä¼ãˆã‚‹ã“ã¨ã«ãªã‚Šã¾ã™ã€‚ review_preferences_action: è¨å®šã®å¤‰æ›´ review_preferences_step: å—ã‘å–りãŸã„メールã®ç¨®é¡žã‚„投稿ã®ãƒ‡ãƒ•ォルト公開範囲ãªã©ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼è¨å®šã‚’å¿…ãšæ¸ˆã¾ã›ã¦ãŠãã¾ã—ょã†ã€‚ç›®ãŒå›žã‚‰ãªã„自信ãŒã‚ã‚‹ãªã‚‰ã€ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ GIF を自動å†ç”Ÿã™ã‚‹è¨å®šã‚‚ã”æ¤œè¨Žãã ã•ã„。 subject: Mastodon ã¸ã‚ˆã†ã“ã - tip_federated_timeline: 連åˆã‚¿ã‚¤ãƒ ライン㯠Mastodon ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã®æµã‚Œã‚’見られるもã®ã§ã™ã€‚ãŸã ã—ã‚ãªãŸã¨åŒã˜ã‚µãƒ¼ãƒãƒ¼ã®äººãŒãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„る人やã€ãƒªãƒ¬ãƒ¼ã‚’経由ã—ã¦é€ã‚‰ã‚Œã¦ãる投稿ã ã‘ãŒå«ã¾ã‚Œã‚‹ã®ã§ã€ãれãŒå…¨ã¦ã§ã¯ã‚りã¾ã›ã‚“。 - tip_following: 最åˆã¯ã€ã‚µãƒ¼ãƒãƒ¼ã®ç®¡ç†è€…ãŒæŒ‡å®šã—ãŸã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’フォãƒãƒ¼ã—ãŸçŠ¶æ…‹ã«ãªã£ã¦ã„ã¾ã™ã€‚ã‚‚ã£ã¨èˆˆå‘³ã®ã‚る人ãŸã¡ã‚’見ã¤ã‘ã‚‹ã«ã¯ã€#QOTO ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚¿ã‚¤ãƒ ラインã¨é€£åˆã‚¿ã‚¤ãƒ ラインを確èªã—ã¦ã¿ã¾ã—ょã†ã€‚ + tip_federated_timeline: 連åˆã‚¿ã‚¤ãƒ ライン㯠Mastodon ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã®æµã‚Œã‚’見られるもã®ã§ã™ã€‚ãŸã ã—ã‚ãªãŸã¨åŒã˜ã‚µãƒ¼ãƒãƒ¼ã®äººãŒãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„る人ã ã‘ãŒå«ã¾ã‚Œã‚‹ã®ã§ã€ãれãŒå…¨ã¦ã§ã¯ã‚りã¾ã›ã‚“。 + tip_following: 最åˆã¯ã€ã‚µãƒ¼ãƒãƒ¼ã®ç®¡ç†è€…をフォãƒãƒ¼ã—ãŸçŠ¶æ…‹ã«ãªã£ã¦ã„ã¾ã™ã€‚ã‚‚ã£ã¨èˆˆå‘³ã®ã‚る人ãŸã¡ã‚’見ã¤ã‘ã‚‹ã«ã¯ã€ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインã¨é€£åˆã‚¿ã‚¤ãƒ ラインを確èªã—ã¦ã¿ã¾ã—ょã†ã€‚ tip_local_timeline: ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ライン㯠%{instance} ã«ã„ã‚‹äººã€…ã®æµã‚Œã‚’見られるもã®ã§ã™ã€‚彼らã¯ã‚ãªãŸã¨åŒã˜ã‚µãƒ¼ãƒãƒ¼ã«ã„る隣人ã®ã‚ˆã†ãªã‚‚ã®ã§ã™ï¼ tip_mobile_webapp: ãŠä½¿ã„ã®ãƒ¢ãƒã‚¤ãƒ«ç«¯æœ«ã§ã€ãƒ–ラウザã‹ã‚‰ Mastodon をホーム画é¢ã«è¿½åŠ ã§ãã¾ã™ã‹ï¼Ÿ ã‚‚ã—è¿½åŠ ã§ãã‚‹å ´åˆã€ãƒ—ッシュ通知ã®å—ã‘å–りãªã©ã€ã¾ã‚‹ã§ã€Œæ™®é€šã®ã€ã‚¢ãƒ—リã®ã‚ˆã†ãªæ©Ÿèƒ½ãŒæ¥½ã—ã‚ã¾ã™ï¼ tips: è±†çŸ¥è˜ diff --git a/streaming/index.js b/streaming/index.js index e055296923cbcce58ea8c8954e95bf50a585bff1..f175794bdb919efa92c0d27659f3807ee4f4f114 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -374,6 +374,8 @@ const startWorker = (workerId) => { return onlyMedia ? 'public:domain:media' : 'public:domain'; case '/api/v1/streaming/hashtag': return 'hashtag'; + case '/api/v1/streaming/hashtag/local': + return 'hashtag:local'; case '/api/v1/streaming/direct': return 'direct'; case '/api/v1/streaming/list': @@ -393,6 +395,7 @@ const startWorker = (workerId) => { 'group', 'group:media', 'hashtag', + 'hashtag:local', ]; /** @@ -765,6 +768,13 @@ const startWorker = (workerId) => { options: { needsFiltering: true, notificationOnly: false }, }); + break; + case 'public:local': + resolve({ + channelIds: ['timeline:public:local'], + options: { needsFiltering: true, notificationOnly: false }, + }); + break; case 'public:remote': resolve({ @@ -812,6 +822,13 @@ const startWorker = (workerId) => { options: { needsFiltering: true, notificationOnly: false }, }); + break; + case 'public:local:media': + resolve({ + channelIds: ['timeline:public:local:media'], + options: { needsFiltering: true, notificationOnly: false }, + }); + break; case 'public:remote:media': resolve({ @@ -859,6 +876,17 @@ const startWorker = (workerId) => { }); } + break; + case 'hashtag:local': + if (!params.tag || params.tag.length === 0) { + reject('No tag for stream provided'); + } else { + resolve({ + channelIds: [`timeline:hashtag:${params.tag.toLowerCase()}:local`], + options: { needsFiltering: true, notificationOnly: false }, + }); + } + break; case 'list': authorizeListAccess(params.list, req).then(() => { @@ -884,7 +912,7 @@ const startWorker = (workerId) => { const streamNameFromChannelName = (channelName, params) => { if (channelName === 'list') { return [channelName, params.list]; - } else if (channelName === 'hashtag') { + } else if (['hashtag', 'hashtag:local'].includes(channelName)) { return [channelName, params.tag]; } else if (['public:domain', 'public:domain:media'].includes(channelName)) { return [channelName, params.domain];