diff options
author | Fire Demon <firedemon@creature.cafe> | 2020-06-30 17:33:55 -0500 |
---|---|---|
committer | Fire Demon <firedemon@creature.cafe> | 2020-08-30 05:41:03 -0500 |
commit | eaf9bc1a428b338ee666f1da1e32eed7e3b6b25e (patch) | |
tree | aeec5fdde79d6e4fa354da326a540811b5576907 /app/javascript | |
parent | 5d5d88e4f65df4c190afeb407167c153584be108 (diff) |
[Feature] Add in-place post editing
Diffstat (limited to 'app/javascript')
20 files changed, 130 insertions, 7 deletions
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index f83738093..4c2cca9eb 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -147,6 +147,9 @@ export function submitCompose(routerHistory) { let media = getState().getIn(['compose', 'media_attachments']); const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']); let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : ''; + const id = getState().getIn(['compose', 'id'], null); + const submit_url = id ? `/api/v1/statuses/${id}` : '/api/v1/statuses'; + const submit_action = (res, body, config) => id ? api(getState).put(res, body, config) : api(getState).post(res, body, config); if ((!status || !status.length) && media.size === 0) { return; @@ -156,7 +159,7 @@ export function submitCompose(routerHistory) { if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) { status = status + ' 👁️'; } - api(getState).post('/api/v1/statuses', { + submit_action(submit_url, { status, content_type: getState().getIn(['compose', 'content_type']), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 05955963c..b71ee3150 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -53,9 +53,12 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.poll = status.poll.id; } + const oldUpdatedAt = normalOldStatus ? normalOldStatus.updated_at || normalOldStatus.created_at : null; + const newUpdatedAt = normalStatus ? normalStatus.updated_at || normalStatus.created_at : null; + // Only calculate these values when status first encountered // Otherwise keep the ones already in the reducer - if (normalOldStatus) { + if (normalOldStatus && oldUpdatedAt === newUpdatedAt) { normalStatus.search_index = normalOldStatus.get('search_index'); normalStatus.contentHtml = normalOldStatus.get('contentHtml'); normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 4d2bda78b..72e8f14d8 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -55,6 +55,24 @@ export function fetchStatus(id) { }; }; +export function editStatus(status, routerHistory) { + return (dispatch, getState) => { + const id = status.get('id'); + + dispatch(fetchContext(id)); + dispatch(fetchStatusRequest(id, false)); + + api(getState).get(`/api/v1/statuses/${id}`, { params: { source: 1 } }).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(fetchStatusSuccess(false)); + dispatch(redraft(status, response.data.text, response.data.content_type, true)); + ensureComposeIsVisible(getState, routerHistory); + }).catch(error => { + dispatch(fetchStatusFail(id, error, false)); + }); + }; +}; + export function fetchStatusSuccess(skipLoading) { return { type: STATUS_FETCH_SUCCESS, @@ -72,12 +90,13 @@ export function fetchStatusFail(id, error, skipLoading) { }; }; -export function redraft(status, raw_text, content_type) { +export function redraft(status, raw_text, content_type, inplace = false) { return { type: REDRAFT, status, raw_text, content_type, + inplace, }; }; diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index 35db5dcc9..295896e55 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -18,6 +18,7 @@ import { } from './announcements'; import { fetchFilters } from './filters'; import { getLocale } from 'mastodon/locales'; +import { resetCompose } from 'flavours/glitch/actions/compose'; const { messages } = getLocale(); @@ -96,6 +97,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti case 'announcement.delete': dispatch(deleteAnnouncement(data.payload)); break; + case 'refresh': + dispatch(resetCompose()); + window.location.reload(); + break; } }, }; diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 4e628a420..5d3789b24 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -73,6 +73,7 @@ class Status extends ImmutablePureComponent { onReblog: PropTypes.func, onBookmark: PropTypes.func, onDelete: PropTypes.func, + onEdit: PropTypes.func, onDirect: PropTypes.func, onMention: PropTypes.func, onPin: PropTypes.func, diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index c314c5fd5..ce1c8df2c 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -12,6 +12,7 @@ import { accountAdminLink, statusAdminLink } from 'flavours/glitch/util/backend_ const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, + edit: { id: 'status.edit', defaultMessage: 'Edit' }, direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, @@ -61,6 +62,7 @@ class StatusActionBar extends ImmutablePureComponent { onFavourite: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, + onEdit: PropTypes.func, onDirect: PropTypes.func, onMention: PropTypes.func, onMute: PropTypes.func, @@ -123,7 +125,7 @@ class StatusActionBar extends ImmutablePureComponent { _openInteractionDialog = type => { window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); - } + } handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); @@ -133,6 +135,10 @@ class StatusActionBar extends ImmutablePureComponent { this.props.onDelete(this.props.status, this.context.router.history, true); } + handleEditClick = () => { + this.props.onEdit(this.props.status, this.context.router.history); + } + handlePinClick = () => { this.props.onPin(this.props.status); } @@ -233,6 +239,7 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); + menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index a39f747b8..b353b028b 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { isRtl } from 'flavours/glitch/util/rtl'; import { FormattedMessage } from 'react-intl'; import Permalink from './permalink'; +import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import classnames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; import { autoPlayGif } from 'flavours/glitch/util/initial_state'; @@ -275,6 +276,20 @@ export default class StatusContent extends React.PureComponent { const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; + const edited = (status.get('edited') === 0) ? null : ( + <div className='status__edit-notice'> + <FormattedMessage + id='status.edited' + defaultMessage='{count, plural, one {# edit} other {# edits}} · last update: {updated_at}' + key={`edit-${status.get('id')}`} + values={{ + count: status.get('edited'), + updated_at: <RelativeTimestamp timestamp={status.get('updated_at')} />, + }} + /> + </div> + ); + const content = { __html: status.get('contentHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') }; const directionStyle = { direction: 'ltr' }; @@ -340,6 +355,7 @@ export default class StatusContent extends React.PureComponent { </button> </p> + {edited} {mentionsPlaceholder} <div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}> @@ -366,6 +382,7 @@ export default class StatusContent extends React.PureComponent { tabIndex='0' ref={this.setRef} > + {edited} <div ref={this.setContentsRef} key={`contents-${tagLinks}-${rewriteMentions}`} @@ -384,6 +401,7 @@ export default class StatusContent extends React.PureComponent { tabIndex='0' ref={this.setRef} > + {edited} <div ref={this.setContentsRef} key={`contents-${tagLinks}`} className='status__content__text' dangerouslySetInnerHTML={content} tabIndex='0' /> {media} </div> diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 2cbe3d094..9e011ac6b 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -17,7 +17,7 @@ import { pin, unpin, } from 'flavours/glitch/actions/interactions'; -import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses'; +import { muteStatus, unmuteStatus, deleteStatus, editStatus } 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'; @@ -166,6 +166,10 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ } }, + onEdit (status, history) { + dispatch(editStatus(status, history)); + }, + onDirect (account, router) { dispatch(directCompose(account, router)); }, diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 080362dd0..c4f510184 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -10,6 +10,7 @@ import { accountAdminLink, statusAdminLink } from 'flavours/glitch/util/backend_ const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, + edit: { id: 'status.edit', defaultMessage: 'Edit' }, direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, reply: { id: 'status.reply', defaultMessage: 'Reply' }, @@ -50,6 +51,7 @@ class ActionBar extends React.PureComponent { onMuteConversation: PropTypes.func, onBlock: PropTypes.func, onDelete: PropTypes.func.isRequired, + onEdit: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, onReport: PropTypes.func, @@ -82,6 +84,10 @@ class ActionBar extends React.PureComponent { this.props.onDelete(this.props.status, this.context.router.history, true); } + handleEditClick = () => { + this.props.onEdit(this.props.status, this.context.router.history); + } + handleDirectClick = () => { this.props.onDirect(this.props.status.get('account'), this.context.router.history); } @@ -164,6 +170,7 @@ class ActionBar extends React.PureComponent { menu.push(null); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); + menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js index 9d11f37e0..eeafc0b08 100644 --- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js +++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js @@ -17,6 +17,7 @@ import { import { muteStatus, unmuteStatus, + editStatus, deleteStatus, hideStatus, revealStatus, @@ -118,6 +119,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onEdit (status, history) { + dispatch(editStatus(status, history)); + }, + onDirect (account, router) { dispatch(directCompose(account, router)); }, diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 3e2e95f35..beea64341 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -26,7 +26,7 @@ import { directCompose, } from 'flavours/glitch/actions/compose'; import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; -import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses'; +import { muteStatus, unmuteStatus, deleteStatus, editStatus } 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'; @@ -304,6 +304,10 @@ class Status extends ImmutablePureComponent { } } + handleEditClick = (status, history) => { + this.props.dispatch(editStatus(status, history)); + } + handleDirectClick = (account, router) => { this.props.dispatch(directCompose(account, router)); } @@ -588,6 +592,7 @@ class Status extends ImmutablePureComponent { onReblog={this.handleReblogClick} onBookmark={this.handleBookmarkClick} onDelete={this.handleDeleteClick} + onEdit={this.handleEditClick} onDirect={this.handleDirectClick} onMention={this.handleMentionClick} onMute={this.handleMuteClick} diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 478883f91..5f53361fa 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -66,6 +66,7 @@ const initialState = ImmutableMap({ do_not_federate: false, threaded_mode: false, }), + id: null, sensitive: false, elefriend: Math.random() < glitchProbability ? Math.floor(Math.random() * totalElefriends) : totalElefriends, spoiler: false, @@ -149,6 +150,7 @@ function apiStatusToTextHashtags (state, status) { function clearAll(state) { return state.withMutations(map => { + map.set('id', null); map.set('text', ''); if (defaultContentType) map.set('content_type', defaultContentType); map.set('spoiler', false); @@ -404,8 +406,10 @@ export default function compose(state = initialState, action) { }); case COMPOSE_REPLY_CANCEL: state = state.setIn(['advanced_options', 'threaded_mode'], false); + // eslint-disable-next-line no-fallthrough case COMPOSE_RESET: return state.withMutations(map => { + map.set('id', null); map.set('in_reply_to', null); if (defaultContentType) map.set('content_type', defaultContentType); map.set('text', ''); @@ -505,6 +509,7 @@ export default function compose(state = initialState, action) { let text = action.raw_text || unescapeHTML(expandMentions(action.status)); if (do_not_federate) text = text.replace(/ ?👁\ufe0f?\u200b?$/, ''); return state.withMutations(map => { + map.set('id', action.inplace ? action.status.get('id') : null); map.set('text', text); map.set('content_type', action.content_type || 'text/plain'); map.set('in_reply_to', action.status.get('in_reply_to_id')); diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss index af73feb89..2994d7aff 100644 --- a/app/javascript/flavours/glitch/styles/index.scss +++ b/app/javascript/flavours/glitch/styles/index.scss @@ -23,3 +23,5 @@ @import 'accessibility'; @import 'rtl'; @import 'dashboard'; + +@import 'monsterfork/index'; diff --git a/app/javascript/flavours/glitch/styles/monsterfork/components/index.scss b/app/javascript/flavours/glitch/styles/monsterfork/components/index.scss new file mode 100644 index 000000000..827779123 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/monsterfork/components/index.scss @@ -0,0 +1 @@ +@import 'status'; diff --git a/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss b/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss new file mode 100644 index 000000000..e64f21a21 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss @@ -0,0 +1,8 @@ +.status__edit-notice { + & > span { + color: $dark-text-color; + line-height: normal; + font-style: italic; + font-size: 12px; + } +} diff --git a/app/javascript/flavours/glitch/styles/monsterfork/index.scss b/app/javascript/flavours/glitch/styles/monsterfork/index.scss new file mode 100644 index 000000000..841415620 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/monsterfork/index.scss @@ -0,0 +1 @@ +@import 'components/index'; diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index dca44917a..d0a55538f 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -53,9 +53,12 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.poll = status.poll.id; } + const oldUpdatedAt = normalOldStatus ? normalOldStatus.updated_at || normalOldStatus.created_at : null; + const newUpdatedAt = normalStatus ? normalStatus.updated_at || normalStatus.created_at : null; + // Only calculate these values when status first encountered // Otherwise keep the ones already in the reducer - if (normalOldStatus) { + if (normalOldStatus && oldUpdatedAt === newUpdatedAt) { normalStatus.search_index = normalOldStatus.get('search_index'); normalStatus.contentHtml = normalOldStatus.get('contentHtml'); normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index beb5c6a4a..1adc1b815 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -18,6 +18,7 @@ import { } from './announcements'; import { fetchFilters } from './filters'; import { getLocale } from '../locales'; +import { resetCompose } from '../actions/compose'; const { messages } = getLocale(); @@ -96,6 +97,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti case 'announcement.delete': dispatch(deleteAnnouncement(data.payload)); break; + case 'refresh': + dispatch(resetCompose()); + window.location.reload(); + break; } }, }; diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 3200f2d82..df05d8515 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { isRtl } from '../rtl'; import { FormattedMessage } from 'react-intl'; import Permalink from './permalink'; +import RelativeTimestamp from './relative_timestamp'; import classnames from 'classnames'; import PollContainer from 'mastodon/containers/poll_container'; import Icon from 'mastodon/components/icon'; @@ -180,6 +181,20 @@ export default class StatusContent extends React.PureComponent { return null; } + const edited = (status.get('edited') === 0) ? null : ( + <div className='status__edit-notice'> + <FormattedMessage + id='status.edited' + defaultMessage='{count, plural, one {# edit} other {# edits}} · last update: {updated_at}' + key={`edit-${status.get('id')}`} + values={{ + count: status.get('edited'), + updated_at: <RelativeTimestamp timestamp={status.get('updated_at')} />, + }} + /> + </div> + ); + const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const renderReadMore = this.props.onClick && status.get('collapsed'); const renderViewThread = this.props.showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']); @@ -232,6 +247,7 @@ export default class StatusContent extends React.PureComponent { <button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button> </p> + {edited} {mentionsPlaceholder} <div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} /> @@ -244,6 +260,8 @@ export default class StatusContent extends React.PureComponent { } else if (this.props.onClick) { const output = [ <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'> + {edited} + <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} /> {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} @@ -260,6 +278,8 @@ export default class StatusContent extends React.PureComponent { } else { return ( <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}> + {edited} + <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} /> {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 025ae6e7d..7702c8be1 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -380,6 +380,7 @@ "status.delete": "Delete", "status.detailed_status": "Detailed conversation view", "status.direct": "Direct message @{name}", + "status.edited": "{count, plural, one {# edit} other {# edits}} · last update: {updated_at}", "status.embed": "Embed", "status.favourite": "Favourite", "status.filtered": "Filtered", |