Merge branch 'heads/origin' into base
This commit is contained in:
commit
a081a02c83
12
Gemfile.lock
12
Gemfile.lock
@ -148,6 +148,7 @@ GEM
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
base64 (0.1.1)
|
||||
bcp47_spec (0.2.1)
|
||||
bcrypt (3.1.18)
|
||||
better_errors (2.10.1)
|
||||
erubi (>= 1.0.0)
|
||||
@ -377,19 +378,19 @@ GEM
|
||||
ipaddress (0.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
json-canonicalization (0.4.0)
|
||||
json-canonicalization (1.0.0)
|
||||
json-jwt (1.15.3)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
bindata
|
||||
httpclient
|
||||
json-ld (3.2.5)
|
||||
json-ld (3.3.1)
|
||||
htmlentities (~> 4.3)
|
||||
json-canonicalization (~> 0.3, >= 0.3.2)
|
||||
json-canonicalization (~> 1.0)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
multi_json (~> 1.15)
|
||||
rack (>= 2.2, < 4)
|
||||
rdf (~> 3.2, >= 3.2.10)
|
||||
rdf (~> 3.3)
|
||||
json-ld-preloaded (3.2.2)
|
||||
json-ld (~> 3.2)
|
||||
rdf (~> 3.2)
|
||||
@ -593,7 +594,8 @@ GEM
|
||||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rdf (3.2.11)
|
||||
rdf (3.3.1)
|
||||
bcp47_spec (~> 0.2)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
rdf-normalize (0.6.1)
|
||||
rdf (~> 3.2)
|
||||
|
@ -62,13 +62,13 @@ export const loadPending = () => ({
|
||||
export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||
return (dispatch, getState) => {
|
||||
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
|
||||
const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : notification.type === 'mention' && notification.status.visibility === 'direct'? activeFilter ==='direct': notification.type;
|
||||
const showAlert = notification.type === 'mention' && notification.status.visibility === 'direct'? getState().getIn(['settings', 'notifications', 'alerts', 'direct'], true) : getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
||||
const playSound = notification.type === 'mention' && notification.status.visibility === 'direct'? getState().getIn(['settings', 'notifications', 'sounds', 'direct'], true) : getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
||||
const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type;
|
||||
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
||||
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
||||
|
||||
let filtered = false;
|
||||
|
||||
if (['mention', 'status'].includes(notification.type) && notification.status.filtered) {
|
||||
if (['mention', 'status', 'direct'].includes(notification.type) && notification.status.filtered) {
|
||||
const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications'));
|
||||
|
||||
if (filters.some(result => result.filter.filter_action === 'hide')) {
|
||||
@ -142,7 +142,7 @@ const excludeTypesFromFilter = filter => {
|
||||
'admin.report',
|
||||
]);
|
||||
|
||||
return allTypes.filterNot(item => filter === 'direct'? item ==='mention':item === filter ).toJS();
|
||||
return allTypes.filterNot(item => item === filter).toJS();
|
||||
};
|
||||
|
||||
const noOp = () => {};
|
||||
|
@ -145,10 +145,10 @@ export function fillTimelineGaps(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 expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
|
||||
export const expandAccountDirectTimeline = (accountId, { maxId} = {}) => expandTimeline(`account:${accountId}:with_replies`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: false, exclude_reblogs: true, max_id: maxId });
|
||||
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { no_direct: true, exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
|
||||
export const expandAccountDirectTimeline = (accountId, { maxId} = {}) => expandTimeline(`account:${accountId}:with_replies`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: false, only_direct: true, exclude_reblogs: true, max_id: maxId });
|
||||
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
|
||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, no_direct: 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, local } = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
|
@ -251,11 +251,11 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
|
||||
handleStatusClick = (e) => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`+`/${this.props.status.get('id')}`)
|
||||
}
|
||||
e.stopPropagation();
|
||||
// if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
|
||||
// e.preventDefault();
|
||||
// this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`+`/${this.props.status.get('id')}`)
|
||||
// }
|
||||
// e.stopPropagation();
|
||||
};
|
||||
handleDeployPictureInPicture = (type, mediaProps) => {
|
||||
const { deployPictureInPicture } = this.props;
|
||||
|
@ -8,9 +8,6 @@ import { debounce } from 'lodash';
|
||||
import RegenerationIndicator from 'mastodon/components/regeneration_indicator';
|
||||
|
||||
import StatusContainer from '../containers/status_container';
|
||||
import StatusContainerWithoutDm from '../containers/status_container_without_dm';
|
||||
import StatusContainerWithDm from '../containers/status_container_with_dm';
|
||||
|
||||
|
||||
import { LoadGap } from './load_gap';
|
||||
import ScrollableList from './scrollable_list';
|
||||
@ -101,44 +98,18 @@ export default class StatusList extends ImmutablePureComponent {
|
||||
maxId={index > 0 ? statusIds.get(index - 1) : null}
|
||||
onClick={onLoadMore}
|
||||
/>
|
||||
) :
|
||||
(timelineId == 'account' || timelineId == 'account_direct' ?
|
||||
(
|
||||
timelineId == 'account' ?
|
||||
(<StatusContainerWithoutDm
|
||||
key={statusId}
|
||||
id={statusId}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType={timelineId}
|
||||
scrollKey={this.props.scrollKey}
|
||||
showThread
|
||||
withCounters={this.props.withCounters}
|
||||
/>) :
|
||||
|
||||
(<StatusContainerWithDm
|
||||
key={statusId}
|
||||
id={statusId}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType={timelineId}
|
||||
scrollKey={this.props.scrollKey}
|
||||
showThread
|
||||
withCounters={this.props.withCounters}
|
||||
/>)
|
||||
|
||||
)
|
||||
:
|
||||
<StatusContainer
|
||||
key={statusId}
|
||||
id={statusId}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType={timelineId}
|
||||
scrollKey={this.props.scrollKey}
|
||||
showThread
|
||||
withCounters={this.props.withCounters}
|
||||
/>))
|
||||
) : (
|
||||
<StatusContainer
|
||||
key={statusId}
|
||||
id={statusId}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType={timelineId}
|
||||
scrollKey={this.props.scrollKey}
|
||||
showThread
|
||||
withCounters={this.props.withCounters}
|
||||
/>
|
||||
))
|
||||
) : null;
|
||||
|
||||
if (scrollableContent && featuredStatusIds) {
|
||||
|
@ -1,287 +0,0 @@
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
unmuteAccount,
|
||||
unblockAccount,
|
||||
} from '../actions/accounts';
|
||||
import { showAlertForError } from '../actions/alerts';
|
||||
import { initBlockModal } from '../actions/blocks';
|
||||
import { initBoostModal } from '../actions/boosts';
|
||||
import {
|
||||
replyCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
} from '../actions/compose';
|
||||
import {
|
||||
blockDomain,
|
||||
unblockDomain,
|
||||
} from '../actions/domain_blocks';
|
||||
import {
|
||||
initAddFilter,
|
||||
} from '../actions/filters';
|
||||
import {
|
||||
reblog,
|
||||
favourite,
|
||||
bookmark,
|
||||
unreblog,
|
||||
unfavourite,
|
||||
unbookmark,
|
||||
pin,
|
||||
unpin,
|
||||
} from '../actions/interactions';
|
||||
import { openModal } from '../actions/modal';
|
||||
import { initMuteModal } from '../actions/mutes';
|
||||
import { deployPictureInPicture } from '../actions/picture_in_picture';
|
||||
import { initReport } from '../actions/reports';
|
||||
import {
|
||||
muteStatus,
|
||||
unmuteStatus,
|
||||
deleteStatus,
|
||||
hideStatus,
|
||||
revealStatus,
|
||||
toggleStatusCollapse,
|
||||
editStatus,
|
||||
translateStatus,
|
||||
undoStatusTranslation,
|
||||
} from '../actions/statuses';
|
||||
import Status from '../components/status';
|
||||
import { boostModal, deleteModal } from '../initial_state';
|
||||
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
|
||||
|
||||
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? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
|
||||
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?' },
|
||||
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
|
||||
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
const getPictureInPicture = makeGetPictureInPicture();
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
status: getStatus(state, props).get('visibility')==='direct'? getStatus(state, props) : null,
|
||||
nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null,
|
||||
pictureInPicture: getPictureInPicture(state, props),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||
|
||||
onReply (status, router) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
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) {
|
||||
if ((e && e.shiftKey) || !boostModal) {
|
||||
this.onModalReblog(status);
|
||||
} else {
|
||||
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
||||
}
|
||||
},
|
||||
|
||||
onFavourite (status) {
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
dispatch(favourite(status));
|
||||
}
|
||||
},
|
||||
|
||||
onBookmark (status) {
|
||||
if (status.get('bookmarked')) {
|
||||
dispatch(unbookmark(status));
|
||||
} else {
|
||||
dispatch(bookmark(status));
|
||||
}
|
||||
},
|
||||
|
||||
onPin (status) {
|
||||
if (status.get('pinned')) {
|
||||
dispatch(unpin(status));
|
||||
} else {
|
||||
dispatch(pin(status));
|
||||
}
|
||||
},
|
||||
|
||||
onEmbed (status) {
|
||||
dispatch(openModal({
|
||||
modalType: 'EMBED',
|
||||
modalProps: {
|
||||
id: status.get('id'),
|
||||
onError: error => dispatch(showAlertForError(error)),
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
onDelete (status, history, withRedraft = false) {
|
||||
if (!deleteModal) {
|
||||
dispatch(deleteStatus(status.get('id'), history, withRedraft));
|
||||
} else {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
|
||||
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
onEdit (status, history) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.editMessage),
|
||||
confirm: intl.formatMessage(messages.editConfirm),
|
||||
onConfirm: () => dispatch(editStatus(status.get('id'), history)),
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
dispatch(editStatus(status.get('id'), history));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onTranslate (status) {
|
||||
if (status.get('translation')) {
|
||||
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
|
||||
} else {
|
||||
dispatch(translateStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onDirect (account, router) {
|
||||
dispatch(directCompose(account, router));
|
||||
},
|
||||
|
||||
onMention (account, router) {
|
||||
dispatch(mentionCompose(account, router));
|
||||
},
|
||||
|
||||
onOpenMedia (statusId, media, index, lang) {
|
||||
dispatch(openModal({
|
||||
modalType: 'MEDIA',
|
||||
modalProps: { statusId, media, index, lang },
|
||||
}));
|
||||
},
|
||||
|
||||
onOpenVideo (statusId, media, lang, options) {
|
||||
dispatch(openModal({
|
||||
modalType: 'VIDEO',
|
||||
modalProps: { statusId, media, lang, options },
|
||||
}));
|
||||
},
|
||||
|
||||
onBlock (status) {
|
||||
const account = status.get('account');
|
||||
dispatch(initBlockModal(account));
|
||||
},
|
||||
|
||||
onUnblock (account) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
},
|
||||
|
||||
onReport (status) {
|
||||
dispatch(initReport(status.get('account'), status));
|
||||
},
|
||||
|
||||
onAddFilter (status) {
|
||||
dispatch(initAddFilter(status, { contextType }));
|
||||
},
|
||||
|
||||
onMute (account) {
|
||||
dispatch(initMuteModal(account));
|
||||
},
|
||||
|
||||
onUnmute (account) {
|
||||
dispatch(unmuteAccount(account.get('id')));
|
||||
},
|
||||
|
||||
onMuteConversation (status) {
|
||||
if (status.get('muted')) {
|
||||
dispatch(unmuteStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(muteStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onToggleHidden (status) {
|
||||
if (status.get('hidden')) {
|
||||
dispatch(revealStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(hideStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onToggleCollapsed (status, isCollapsed) {
|
||||
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
|
||||
},
|
||||
|
||||
onBlockDomain (domain) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
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. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockDomainConfirm),
|
||||
onConfirm: () => dispatch(blockDomain(domain)),
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
onUnblockDomain (domain) {
|
||||
dispatch(unblockDomain(domain));
|
||||
},
|
||||
|
||||
deployPictureInPicture (status, type, mediaProps) {
|
||||
dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps));
|
||||
},
|
||||
|
||||
onInteractionModal (type, status) {
|
||||
dispatch(openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
type,
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('uri'),
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
@ -1,287 +0,0 @@
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
unmuteAccount,
|
||||
unblockAccount,
|
||||
} from '../actions/accounts';
|
||||
import { showAlertForError } from '../actions/alerts';
|
||||
import { initBlockModal } from '../actions/blocks';
|
||||
import { initBoostModal } from '../actions/boosts';
|
||||
import {
|
||||
replyCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
} from '../actions/compose';
|
||||
import {
|
||||
blockDomain,
|
||||
unblockDomain,
|
||||
} from '../actions/domain_blocks';
|
||||
import {
|
||||
initAddFilter,
|
||||
} from '../actions/filters';
|
||||
import {
|
||||
reblog,
|
||||
favourite,
|
||||
bookmark,
|
||||
unreblog,
|
||||
unfavourite,
|
||||
unbookmark,
|
||||
pin,
|
||||
unpin,
|
||||
} from '../actions/interactions';
|
||||
import { openModal } from '../actions/modal';
|
||||
import { initMuteModal } from '../actions/mutes';
|
||||
import { deployPictureInPicture } from '../actions/picture_in_picture';
|
||||
import { initReport } from '../actions/reports';
|
||||
import {
|
||||
muteStatus,
|
||||
unmuteStatus,
|
||||
deleteStatus,
|
||||
hideStatus,
|
||||
revealStatus,
|
||||
toggleStatusCollapse,
|
||||
editStatus,
|
||||
translateStatus,
|
||||
undoStatusTranslation,
|
||||
} from '../actions/statuses';
|
||||
import Status from '../components/status';
|
||||
import { boostModal, deleteModal } from '../initial_state';
|
||||
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
|
||||
|
||||
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? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
|
||||
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?' },
|
||||
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
|
||||
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
const getPictureInPicture = makeGetPictureInPicture();
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
status: getStatus(state, props).get('visibility')!=='direct'? getStatus(state, props) : null,
|
||||
nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null,
|
||||
pictureInPicture: getPictureInPicture(state, props),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||
|
||||
onReply (status, router) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
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) {
|
||||
if ((e && e.shiftKey) || !boostModal) {
|
||||
this.onModalReblog(status);
|
||||
} else {
|
||||
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
||||
}
|
||||
},
|
||||
|
||||
onFavourite (status) {
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
dispatch(favourite(status));
|
||||
}
|
||||
},
|
||||
|
||||
onBookmark (status) {
|
||||
if (status.get('bookmarked')) {
|
||||
dispatch(unbookmark(status));
|
||||
} else {
|
||||
dispatch(bookmark(status));
|
||||
}
|
||||
},
|
||||
|
||||
onPin (status) {
|
||||
if (status.get('pinned')) {
|
||||
dispatch(unpin(status));
|
||||
} else {
|
||||
dispatch(pin(status));
|
||||
}
|
||||
},
|
||||
|
||||
onEmbed (status) {
|
||||
dispatch(openModal({
|
||||
modalType: 'EMBED',
|
||||
modalProps: {
|
||||
id: status.get('id'),
|
||||
onError: error => dispatch(showAlertForError(error)),
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
onDelete (status, history, withRedraft = false) {
|
||||
if (!deleteModal) {
|
||||
dispatch(deleteStatus(status.get('id'), history, withRedraft));
|
||||
} else {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
|
||||
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
onEdit (status, history) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.editMessage),
|
||||
confirm: intl.formatMessage(messages.editConfirm),
|
||||
onConfirm: () => dispatch(editStatus(status.get('id'), history)),
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
dispatch(editStatus(status.get('id'), history));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onTranslate (status) {
|
||||
if (status.get('translation')) {
|
||||
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
|
||||
} else {
|
||||
dispatch(translateStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onDirect (account, router) {
|
||||
dispatch(directCompose(account, router));
|
||||
},
|
||||
|
||||
onMention (account, router) {
|
||||
dispatch(mentionCompose(account, router));
|
||||
},
|
||||
|
||||
onOpenMedia (statusId, media, index, lang) {
|
||||
dispatch(openModal({
|
||||
modalType: 'MEDIA',
|
||||
modalProps: { statusId, media, index, lang },
|
||||
}));
|
||||
},
|
||||
|
||||
onOpenVideo (statusId, media, lang, options) {
|
||||
dispatch(openModal({
|
||||
modalType: 'VIDEO',
|
||||
modalProps: { statusId, media, lang, options },
|
||||
}));
|
||||
},
|
||||
|
||||
onBlock (status) {
|
||||
const account = status.get('account');
|
||||
dispatch(initBlockModal(account));
|
||||
},
|
||||
|
||||
onUnblock (account) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
},
|
||||
|
||||
onReport (status) {
|
||||
dispatch(initReport(status.get('account'), status));
|
||||
},
|
||||
|
||||
onAddFilter (status) {
|
||||
dispatch(initAddFilter(status, { contextType }));
|
||||
},
|
||||
|
||||
onMute (account) {
|
||||
dispatch(initMuteModal(account));
|
||||
},
|
||||
|
||||
onUnmute (account) {
|
||||
dispatch(unmuteAccount(account.get('id')));
|
||||
},
|
||||
|
||||
onMuteConversation (status) {
|
||||
if (status.get('muted')) {
|
||||
dispatch(unmuteStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(muteStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onToggleHidden (status) {
|
||||
if (status.get('hidden')) {
|
||||
dispatch(revealStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(hideStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onToggleCollapsed (status, isCollapsed) {
|
||||
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
|
||||
},
|
||||
|
||||
onBlockDomain (domain) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
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. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockDomainConfirm),
|
||||
onConfirm: () => dispatch(blockDomain(domain)),
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
onUnblockDomain (domain) {
|
||||
dispatch(unblockDomain(domain));
|
||||
},
|
||||
|
||||
deployPictureInPicture (status, type, mediaProps) {
|
||||
dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps));
|
||||
},
|
||||
|
||||
onInteractionModal (type, status) {
|
||||
dispatch(openModal({
|
||||
modalType: 'INTERACTION',
|
||||
modalProps: {
|
||||
type,
|
||||
accountId: status.getIn(['account', 'id']),
|
||||
url: status.get('uri'),
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
@ -101,7 +101,6 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
|
||||
componentDidMount () {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
if (accountId) {
|
||||
this._load();
|
||||
} else {
|
||||
@ -111,7 +110,6 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { params: { acct }, accountId, dispatch } = this.props;
|
||||
|
||||
if (prevProps.accountId !== accountId && accountId) {
|
||||
this._load();
|
||||
} else if (prevProps.params.acct !== acct) {
|
||||
|
@ -4,7 +4,6 @@ import { PureComponent } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -38,14 +37,14 @@ class ActionBar extends PureComponent {
|
||||
|
||||
render () {
|
||||
const { intl } = this.props;
|
||||
|
||||
const username = this.props.account.get('acct')
|
||||
let menu = [];
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
|
||||
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
|
||||
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.directMessages), to: '/direct_messages' });
|
||||
menu.push({ text: intl.formatMessage(messages.directMessages), to:`/@${username}/direct_messages` });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
|
||||
|
@ -250,7 +250,7 @@ class PrivacyDropdown extends PureComponent {
|
||||
const { value, container, disabled, intl } = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
||||
const valueOption = this.options.find(item => item.value === value);
|
||||
const valueOption = this.options.find(item => item.value === value) || this.options[0];
|
||||
|
||||
return (
|
||||
<div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}>
|
||||
|
@ -1,70 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getStatusList } from 'mastodon/selectors';
|
||||
|
||||
import { fetchDirectStatuses } from '../../actions/direct_statuses';
|
||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import StatusList from '../../components/status_list';
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.pins', defaultMessage: 'Pinned post' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
statusIds: getStatusList(state, 'direct'),
|
||||
hasMore: !!state.getIn(['status_lists', 'direct', 'next']),
|
||||
});
|
||||
|
||||
class PinnedStatuses extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasMore: PropTypes.bool.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
UNSAFE_componentWillMount () {
|
||||
this.props.dispatch(fetchDirectStatuses());
|
||||
}
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
};
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, statusIds, hasMore, multiColumn } = this.props;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}>
|
||||
<ColumnBackButtonSlim />
|
||||
<StatusList
|
||||
statusIds={statusIds}
|
||||
scrollKey='pinned_statuses'
|
||||
hasMore={hasMore}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(PinnedStatuses));
|
File diff suppressed because one or more lines are too long
@ -1,44 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call,
|
||||
@typescript-eslint/no-unsafe-return,
|
||||
@typescript-eslint/no-unsafe-assignment,
|
||||
@typescript-eslint/no-unsafe-member-access
|
||||
-- the settings store is not yet typed */
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { changeSetting } from '../../../actions/settings';
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
|
||||
export const ColumnSettings: React.FC = () => {
|
||||
const settings = useAppSelector((state) => state.settings.get('local'));
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const onChange = useCallback(
|
||||
(key: string, checked: boolean) => {
|
||||
dispatch(changeSetting(['local', ...key], checked));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
prefix='local_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'media']}
|
||||
onChange={onChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='local.column_settings.show_media'
|
||||
defaultMessage='Show Media'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { DismissableBanner } from 'mastodon/components/dismissable_banner';
|
||||
import { title } from 'mastodon/initial_state';
|
||||
|
||||
export const ExplorePrompt = () => (
|
||||
<DismissableBanner id='local.explore_prompt'>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='local.explore_prompt.body'
|
||||
defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:" values={{ title }}
|
||||
/>
|
||||
</p>
|
||||
</DismissableBanner>
|
||||
);
|
@ -1,195 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
|
||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
import { expandHomeTimeline } from '../../actions/timelines';
|
||||
import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import LocalStatusListContainer from '../ui/containers/local_status_list_container';
|
||||
|
||||
import { ColumnSettings } from './components/column_settings';
|
||||
import { ExplorePrompt } from './components/explore_prompt';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.local', defaultMessage: 'local' },
|
||||
});
|
||||
|
||||
const getHomeFeedSpeed = createSelector([
|
||||
state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
|
||||
state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
|
||||
state => state.get('statuses'),
|
||||
], (statusIds, pendingStatusIds, statusMap) => {
|
||||
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
|
||||
const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
|
||||
|
||||
if (statuses.isEmpty()) {
|
||||
return {
|
||||
gap: 0,
|
||||
newest: new Date(0),
|
||||
};
|
||||
}
|
||||
|
||||
const datetimes = statuses.map(status => status.get('created_at', 0));
|
||||
const oldest = new Date(datetimes.min());
|
||||
const newest = new Date(datetimes.max());
|
||||
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
||||
|
||||
return {
|
||||
gap: averageGap,
|
||||
newest,
|
||||
};
|
||||
});
|
||||
|
||||
const homeTooSlow = createSelector([
|
||||
state => state.getIn(['timelines', 'home', 'isLoading']),
|
||||
state => state.getIn(['timelines', 'home', 'isPartial']),
|
||||
getHomeFeedSpeed,
|
||||
], (isLoading, isPartial, speed) =>
|
||||
!isLoading && !isPartial // Only if the home feed has finished loading
|
||||
&& (
|
||||
(speed.gap > (30 * 60) // If the average gap between posts is more than 30 minutes
|
||||
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||
)
|
||||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
|
||||
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
|
||||
tooSlow: homeTooSlow(state),
|
||||
});
|
||||
|
||||
class HomeTimeline extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
isPartial: PropTypes.bool,
|
||||
columnId: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
tooSlow: PropTypes.bool,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('HOME', {}));
|
||||
}
|
||||
};
|
||||
|
||||
handleMove = (dir) => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
};
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
};
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
};
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
this.props.dispatch(expandHomeTimeline({ maxId }));
|
||||
};
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
this._checkIfReloadNeeded(prevProps.isPartial, this.props.isPartial);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._stopPolling();
|
||||
}
|
||||
|
||||
_checkIfReloadNeeded (wasPartial, isPartial) {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
if (wasPartial === isPartial) {
|
||||
return;
|
||||
} else if (!wasPartial && isPartial) {
|
||||
this.polling = setInterval(() => {
|
||||
dispatch(expandHomeTimeline());
|
||||
}, 3000);
|
||||
} else if (wasPartial && !isPartial) {
|
||||
this._stopPolling();
|
||||
}
|
||||
}
|
||||
|
||||
_stopPolling () {
|
||||
if (this.polling) {
|
||||
clearInterval(this.polling);
|
||||
this.polling = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
render () {
|
||||
const { intl, hasUnread, columnId, multiColumn, tooSlow } = this.props;
|
||||
const pinned = !!columnId;
|
||||
const { signedIn } = this.context.identity;
|
||||
const banners = [];
|
||||
|
||||
|
||||
if (tooSlow) {
|
||||
banners.push(<ExplorePrompt key='explore-prompt' />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
icon='home'
|
||||
active={hasUnread}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
>
|
||||
<ColumnSettings />
|
||||
</ColumnHeader>
|
||||
|
||||
{signedIn ? (
|
||||
<LocalStatusListContainer
|
||||
prepend={banners}
|
||||
alwaysPrepend
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`home_timeline-${columnId}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
timelineId='home'
|
||||
emptyMessage={<FormattedMessage id='empty_column.local' defaultMessage='Your home timeline is empty! Follow more people to fill it up.' />}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
) : <NotSignedInIndicator />}
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(HomeTimeline));
|
@ -54,14 +54,12 @@ const getNotifications = createSelector([
|
||||
state => state.getIn(['notifications', 'items']),
|
||||
], (showFilterBar, allowedType, excludedTypes, notifications) => {
|
||||
if (!showFilterBar || allowedType === 'all') {
|
||||
return notifications.filterNot(item => item !== null && (excludedTypes.includes(item.get('type')) || ('mention' === item.get('type') && item.get('visibility') === 'direct' && excludedTypes.includes('direct'))));
|
||||
// used if user changed the notification settings after loading the notifications from the server
|
||||
// otherwise a list of notifications will come pre-filtered from the backend
|
||||
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
|
||||
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
|
||||
}
|
||||
else if (allowedType === 'direct')
|
||||
return notifications.filter(item => item === null || ('mention' === item.get('type') && item.get('visibility') === 'direct'));
|
||||
else if (allowedType === 'mention')
|
||||
return notifications.filter(item => item === null || ('mention' === item.get('type') && item.get('visibility') !== 'direct'));
|
||||
else
|
||||
return notifications.filter(item => item === null || allowedType === item.get('type'))
|
||||
return notifications.filter(item => item === null || allowedType === item.get('type'));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
@ -192,7 +190,7 @@ class Notifications extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission, allowedType, excludedTypes } = this.props;
|
||||
const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
|
||||
const pinned = !!columnId;
|
||||
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here." />;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
@ -75,7 +75,7 @@ class NavigationPanel extends Component {
|
||||
</>
|
||||
)}
|
||||
{(signedIn) && (
|
||||
<ColumnLink transparent to='/local' isActive={this.isFirehoseActive} icon='hashtag' text={intl.formatMessage(messages.firehose)} />
|
||||
<ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='hashtag' text={intl.formatMessage(messages.firehose)} />
|
||||
)}
|
||||
|
||||
{!signedIn && (
|
||||
|
@ -1,66 +0,0 @@
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { scrollTopTimeline, loadPending } from '../../../actions/timelines';
|
||||
import StatusList from '../../../components/status_list';
|
||||
|
||||
const makeGetStatusIds = (pending = false) => createSelector([
|
||||
(state) => state.getIn(['settings', 'local'], ImmutableMap()),
|
||||
(state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()),
|
||||
(state) => state.get('statuses'),
|
||||
(type) => type
|
||||
], (columnSettings, statusIds, statuses,type) => {
|
||||
return statusIds.filter(id => {
|
||||
if (id === null) return true;
|
||||
|
||||
const statusForId = statuses.get(id);
|
||||
|
||||
let showStatus = true;
|
||||
|
||||
if (statusForId.get('visibility') === 'direct')
|
||||
return false;
|
||||
if (statusForId.get('in_reply_to_id')) {
|
||||
showStatus = showStatus && statusForId.get('in_reply_to_account_id') === statusForId.get('account')
|
||||
}
|
||||
if (columnSettings.getIn(['shows', 'media']) === true) {
|
||||
showStatus = showStatus && statusForId.get('media_attachments').size>0;
|
||||
}
|
||||
|
||||
return showStatus;
|
||||
});
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatusIds = makeGetStatusIds();
|
||||
const getPendingStatusIds = makeGetStatusIds(true);
|
||||
|
||||
const mapStateToProps = (state, { timelineId }) => ({
|
||||
statusIds: getStatusIds(state, { type: timelineId }),
|
||||
lastId: state.getIn(['timelines', timelineId, 'items'])?.last(),
|
||||
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
|
||||
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
||||
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
|
||||
numPending: getPendingStatusIds(state, { type: timelineId }).size,
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { timelineId }) => ({
|
||||
|
||||
onScrollToTop: debounce(() => {
|
||||
dispatch(scrollTopTimeline(timelineId, true));
|
||||
}, 100),
|
||||
|
||||
onScroll: debounce(() => {
|
||||
dispatch(scrollTopTimeline(timelineId, false));
|
||||
}, 100),
|
||||
|
||||
onLoadPending: () => dispatch(loadPending(timelineId)),
|
||||
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);
|
@ -20,8 +20,6 @@ const makeGetStatusIds = (pending = false) => createSelector([
|
||||
const statusForId = statuses.get(id);
|
||||
let showStatus = true;
|
||||
|
||||
if (statusForId.get('visibility') === 'direct') return false;
|
||||
|
||||
if (statusForId.get('account') === me) return true;
|
||||
|
||||
if (columnSettings.getIn(['shows', 'reblog']) === false) {
|
||||
|
@ -62,8 +62,7 @@ import {
|
||||
Explore,
|
||||
Onboarding,
|
||||
About,
|
||||
PrivacyPolicy,
|
||||
SendDirectMessagesStatuses
|
||||
PrivacyPolicy
|
||||
} from './util/async-components';
|
||||
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||
|
||||
@ -197,13 +196,10 @@ class SwitchingColumnsArea extends PureComponent {
|
||||
|
||||
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
||||
|
||||
{/* <Redirect from='/timelines/public' to='/public' exact />
|
||||
<Redirect from='/timelines/public' to='/public' exact />
|
||||
<Redirect from='/timelines/public/local' to='/public/local' exact />
|
||||
<WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
|
||||
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} /> */}
|
||||
|
||||
<Redirect from='/timelines/local' to='/local' exact />
|
||||
<WrappedRoute path='/local' exact component={LocalTimeline} content={children} />
|
||||
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} />
|
||||
|
||||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||
@ -213,7 +209,6 @@ class SwitchingColumnsArea extends PureComponent {
|
||||
|
||||
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
||||
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
||||
<WrappedRoute path='/direct_messages' component={SendDirectMessagesStatuses} content={children} />
|
||||
|
||||
<WrappedRoute path='/start' exact component={Onboarding} content={children} />
|
||||
{/* <WrappedRoute path='/directory' component={Directory} content={children} /> */}
|
||||
|
@ -14,9 +14,6 @@ export function HomeTimeline () {
|
||||
return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline');
|
||||
}
|
||||
|
||||
export function LocalTimeline () {
|
||||
return import(/* webpackChunkName: "features/home_timeline" */'../../local_timeline');
|
||||
}
|
||||
export function PublicTimeline () {
|
||||
return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline');
|
||||
}
|
||||
@ -61,10 +58,6 @@ export function PinnedStatuses () {
|
||||
return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses');
|
||||
}
|
||||
|
||||
export function SendDirectMessagesStatuses () {
|
||||
return import(/* webpackChunkName: "features/pinned_statuses" */'../../direct_messages');
|
||||
}
|
||||
|
||||
export function AccountDirectMessages () {
|
||||
return import(/* webpackChunkName: "features/pinned_statuses" */'../../account_direct');
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ const initialState = ImmutableMap({
|
||||
quickFilter: ImmutableMap({
|
||||
active: 'all',
|
||||
show: true,
|
||||
advanced: true,
|
||||
advanced: false,
|
||||
}),
|
||||
|
||||
dismissPermissionBanner: false,
|
||||
|
@ -24,24 +24,23 @@ export type ShortNumber = [number, DecimalUnits, 0 | 1]; // Array of: shorten nu
|
||||
* // => [5.936, 1000, 1]
|
||||
*/
|
||||
export function toShortNumber(sourceNumber: number): ShortNumber {
|
||||
if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
|
||||
return [sourceNumber, DECIMAL_UNITS.ONE, 0];
|
||||
} else if (sourceNumber < DECIMAL_UNITS.MILLION) {
|
||||
return [
|
||||
sourceNumber / DECIMAL_UNITS.THOUSAND,
|
||||
DECIMAL_UNITS.THOUSAND,
|
||||
sourceNumber < TEN_THOUSAND ? 1 : 0,
|
||||
];
|
||||
} else if (sourceNumber < DECIMAL_UNITS.BILLION) {
|
||||
return [
|
||||
sourceNumber / DECIMAL_UNITS.MILLION,
|
||||
DECIMAL_UNITS.MILLION,
|
||||
sourceNumber < TEN_MILLIONS ? 1 : 0,
|
||||
];
|
||||
} else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
|
||||
return [sourceNumber / DECIMAL_UNITS.BILLION, DECIMAL_UNITS.BILLION, 0];
|
||||
}
|
||||
|
||||
// if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
|
||||
// return [sourceNumber, DECIMAL_UNITS.ONE, 0];
|
||||
// } else if (sourceNumber < DECIMAL_UNITS.MILLION) {
|
||||
// return [
|
||||
// sourceNumber / DECIMAL_UNITS.THOUSAND,
|
||||
// DECIMAL_UNITS.THOUSAND,
|
||||
// sourceNumber < TEN_THOUSAND ? 1 : 0,
|
||||
// ];
|
||||
// } else if (sourceNumber < DECIMAL_UNITS.BILLION) {
|
||||
// return [
|
||||
// sourceNumber / DECIMAL_UNITS.MILLION,
|
||||
// DECIMAL_UNITS.MILLION,
|
||||
// sourceNumber < TEN_MILLIONS ? 1 : 0,
|
||||
// ];
|
||||
// } else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
|
||||
// return [sourceNumber / DECIMAL_UNITS.BILLION, DECIMAL_UNITS.BILLION, 0];
|
||||
// }
|
||||
return [sourceNumber, DECIMAL_UNITS.ONE, 0];
|
||||
}
|
||||
|
||||
|
@ -206,11 +206,18 @@ body.theme-bird-ui-light.layout-multiple-columns {
|
||||
--color-accent:#df98a2;
|
||||
--color-accent-dark: #be2a4d;
|
||||
|
||||
--color-border: #e9dee0;
|
||||
--color-dim: #c16c80;
|
||||
|
||||
--color-mud: #f0dee2;
|
||||
--color-light-purple: #df98a2;
|
||||
|
||||
--color-bg-compose-form: rgb(192 108 128 / .2);
|
||||
--color-bg-compose-form-focus: rgb(192 108 128/ .4);
|
||||
|
||||
--color-bg: #fff;
|
||||
--color-fg: #000;
|
||||
|
||||
--color-border: #e9dee0;
|
||||
--color-dim: #c16c80;
|
||||
--color-light-text: #1f1b23;
|
||||
|
||||
--color-green: #17bf63;
|
||||
@ -220,25 +227,24 @@ body.theme-bird-ui-light.layout-multiple-columns {
|
||||
--color-focusable-toot: rgba(0, 0, 0, 0.035);
|
||||
--color-light-shade: #db9e9e12;
|
||||
|
||||
--color-mud: #f0dee2;
|
||||
|
||||
|
||||
--color-black-coral: #9188a6;
|
||||
--color-profile-button-hover: #db9e9e40;
|
||||
--color-column-link-hover: #db9e9e40;
|
||||
--color-profile-button-hover: #f1eff41a;
|
||||
--color-column-link-hover: #f7f7f91a;
|
||||
--color-modal-overlay: #6a5b8366;
|
||||
--color-dark: #f7f9f9;
|
||||
--color-thread-line: #e1e8ed;
|
||||
--color-gainsboro: #8899a6;
|
||||
|
||||
--color-light-purple: #df98a2;
|
||||
|
||||
|
||||
--color-dark-electric-blue: #9088a6;
|
||||
--color-bg-75: #ffffffbf;
|
||||
--color-accent: var(--color-accent-dark);
|
||||
--color-ghost-button-text: var(--color-accent-dark);
|
||||
|
||||
--color-bg-compose-form: rgb(192 108 128 / .2);
|
||||
--color-bg-compose-form-focus: rgb(192 108 128/ .4);
|
||||
|
||||
--color-hashtag: var(--color-accent-dark);
|
||||
--color-mention: var(--color-accent-dark);
|
||||
|
||||
|
@ -19,8 +19,6 @@
|
||||
--color-brand-mastodon-threaded-line: #434264;
|
||||
--color-brand-mastodon-text-light: #8493a7;
|
||||
|
||||
|
||||
|
||||
/* Colors */
|
||||
/* Note: Remember to search for the DIM hex
|
||||
and replace it inside the SVG icons if you decide to change it */
|
||||
@ -239,7 +237,7 @@ body.theme-bird-ui-light.layout-single-column {
|
||||
--color-red: #e0245e;
|
||||
--color-red-75: #e0245ebf;
|
||||
|
||||
--color-light-shade: #db9e9e12;
|
||||
--color-light-shade: #00000005;
|
||||
|
||||
--color-focusable-toot: rgba(0, 0, 0, 0.035);
|
||||
--color-light-text: #1f1b23;
|
||||
@ -248,8 +246,8 @@ body.theme-bird-ui-light.layout-single-column {
|
||||
|
||||
--color-black-coral: #9188a6;
|
||||
|
||||
--color-profile-button-hover: #db9e9e40;
|
||||
--color-column-link-hover: #db9e9e40;
|
||||
--color-profile-button-hover: #1e1b231a;
|
||||
--color-column-link-hover: #1e1b231a;
|
||||
|
||||
--color-modal-overlay: #6a5b8366;
|
||||
--color-dark: #f7f9f9;
|
||||
@ -3014,7 +3012,9 @@ body.embed .button.logo-button:hover,
|
||||
.layout-single-column .column-link[href="/local"] {
|
||||
order: 5;
|
||||
}
|
||||
|
||||
.layout-single-column .column-link[href="/pulic/local"] {
|
||||
order: 5;
|
||||
}
|
||||
.layout-single-column .column-link[href='/follow_requests'] {
|
||||
order: 8;
|
||||
}
|
||||
|
@ -234,13 +234,13 @@ body.theme-default.layout-multiple-columns {
|
||||
--color-red-75: #e0245ebf;
|
||||
--color-focusable-toot: rgba(0, 0, 0, 0.035);
|
||||
|
||||
--color-light-shade: #db9e9e12;
|
||||
--color-light-shade: #00000005;;
|
||||
--color-mud: #f0dee2;
|
||||
|
||||
--color-black-coral: #9188a6;
|
||||
|
||||
--color-profile-button-hover: #db9e9e40;
|
||||
--color-column-link-hover: #db9e9e40;
|
||||
--color-profile-button-hover: #f1eff41a;
|
||||
--color-column-link-hover: #f7f7f91a;
|
||||
|
||||
--color-modal-overlay: #6a5b8366;
|
||||
--color-dark: #f7f9f9;
|
||||
|
@ -58,7 +58,7 @@
|
||||
--color-thread-line: var(--color-brand-mastodon-threaded-line);
|
||||
--color-topaz: #dadaf3;
|
||||
|
||||
--color-light-purple: #df98a2;
|
||||
--color-light-purple: #9baec8;
|
||||
|
||||
--color-lighter-purple: #a5b8d3;
|
||||
--color-dark-electric-blue: #576078;
|
||||
@ -239,43 +239,43 @@ body.theme-default.layout-single-column {
|
||||
--color-accent:#df98a2;
|
||||
--color-accent-dark: #be2a4d;
|
||||
|
||||
--color-bg: #fff;
|
||||
--color-fg: #000;
|
||||
|
||||
--color-border: #e9dee0;
|
||||
--color-dim: #c16c80;
|
||||
|
||||
--color-mud: #f0dee2;
|
||||
--color-light-purple: #df98a2;
|
||||
|
||||
--color-bg-compose-form: rgb(192 108 128 / .2);
|
||||
--color-bg-compose-form-focus: rgb(192 108 128/ .4);
|
||||
|
||||
|
||||
--color-bg: #fff;
|
||||
--color-fg: #000;
|
||||
|
||||
--color-green: #17bf63;
|
||||
--color-red: #e0245e;
|
||||
--color-red-75: #e0245ebf;
|
||||
|
||||
--color-light-shade: #db9e9e12;
|
||||
--color-light-shade: #00000005;;
|
||||
|
||||
--color-focusable-toot: rgba(0, 0, 0, 0.035);
|
||||
--color-light-text: #1f1b23;
|
||||
|
||||
--color-mud: #f0dee2;
|
||||
|
||||
--color-black-coral: #9188a6;
|
||||
|
||||
--color-profile-button-hover: #db9e9e40;
|
||||
--color-column-link-hover: #db9e9e40;
|
||||
--color-profile-button-hover: #1e1b231a;
|
||||
--color-column-link-hover: #1e1b231a;
|
||||
|
||||
--color-modal-overlay: #6a5b8366;
|
||||
--color-dark: #f7f9f9;
|
||||
--color-thread-line: #e1e8ed;
|
||||
--color-topaz: #8899a6;
|
||||
|
||||
--color-light-purple: #df98a2;
|
||||
|
||||
--color-dark-electric-blue: #9088a6;
|
||||
--color-bg-75: #ffffffbf;
|
||||
--color-accent: var(--color-accent-dark);
|
||||
--color-ghost-button-text: var(--color-accent-dark);
|
||||
|
||||
--color-bg-compose-form: rgb(192 108 128 / .2);
|
||||
--color-bg-compose-form-focus: rgb(192 108 128/ .4);
|
||||
|
||||
--color-hashtag: var(--color-accent-dark);
|
||||
--color-mention: var(--color-accent-dark);
|
||||
|
||||
@ -3034,6 +3034,9 @@ body.embed .button.logo-button:hover,
|
||||
.layout-single-column .column-link[href="/local"] {
|
||||
order: 5;
|
||||
}
|
||||
.layout-single-column .column-link[href="/public/local"] {
|
||||
order: 5;
|
||||
}
|
||||
|
||||
.layout-single-column .column-link[href='/follow_requests'] {
|
||||
order: 8;
|
||||
|
@ -7,6 +7,8 @@ class AccountStatusesFilter
|
||||
only_media
|
||||
exclude_replies
|
||||
exclude_reblogs
|
||||
only_direct
|
||||
no_direct
|
||||
).freeze
|
||||
|
||||
attr_reader :params, :account, :current_account
|
||||
@ -20,6 +22,8 @@ class AccountStatusesFilter
|
||||
def results
|
||||
scope = initial_scope
|
||||
|
||||
scope.merge!(direct_scope) if only_direct?
|
||||
scope.merge!(no_direct_scope) if no_direct?
|
||||
scope.merge!(pinned_scope) if pinned?
|
||||
scope.merge!(only_media_scope) if only_media?
|
||||
scope.merge!(no_replies_scope) if exclude_replies?
|
||||
@ -69,6 +73,14 @@ class AccountStatusesFilter
|
||||
)
|
||||
end
|
||||
|
||||
def direct_scope
|
||||
Status.with_direct_visibility
|
||||
end
|
||||
|
||||
def no_direct_scope
|
||||
Status.without_direct_visibility
|
||||
end
|
||||
|
||||
def only_media_scope
|
||||
Status.joins(:media_attachments).merge(account.media_attachments.reorder(nil)).group(Status.arel_table[:id])
|
||||
end
|
||||
@ -123,6 +135,14 @@ class AccountStatusesFilter
|
||||
truthy_param?(:pinned)
|
||||
end
|
||||
|
||||
def only_direct?
|
||||
truthy_param?(:only_direct)
|
||||
end
|
||||
|
||||
def no_direct?
|
||||
truthy_param?(:no_direct)
|
||||
end
|
||||
|
||||
def only_media?
|
||||
truthy_param?(:only_media)
|
||||
end
|
||||
|
@ -3,6 +3,7 @@
|
||||
class Admin::StatusFilter
|
||||
KEYS = %i(
|
||||
media
|
||||
direct
|
||||
report_id
|
||||
).freeze
|
||||
|
||||
@ -33,6 +34,8 @@ class Admin::StatusFilter
|
||||
case key.to_s
|
||||
when 'media'
|
||||
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id).reorder('statuses.id desc')
|
||||
when 'direct'
|
||||
Status.where(visibility: :direct).group(:id).reorder('statuses.id desc')
|
||||
else
|
||||
raise Mastodon::InvalidParameterError, "Unknown filter: #{key}"
|
||||
end
|
||||
|
@ -28,7 +28,7 @@ class Feed
|
||||
unhydrated = redis.zrangebyscore(key, "(#{min_id}", "(#{max_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
|
||||
end
|
||||
|
||||
Status.where(id: unhydrated).cache_ids
|
||||
Status.without_direct_visibility.where(id: unhydrated).cache_ids
|
||||
end
|
||||
|
||||
def key
|
||||
|
@ -20,6 +20,7 @@ class Notification < ApplicationRecord
|
||||
include Paginable
|
||||
|
||||
LEGACY_TYPE_CLASS_MAP = {
|
||||
'Direct' => :direct,
|
||||
'Mention' => :mention,
|
||||
'Status' => :reblog,
|
||||
'Follow' => :follow,
|
||||
@ -29,6 +30,7 @@ class Notification < ApplicationRecord
|
||||
}.freeze
|
||||
|
||||
TYPES = %i(
|
||||
direct
|
||||
mention
|
||||
status
|
||||
reblog
|
||||
@ -44,6 +46,7 @@ class Notification < ApplicationRecord
|
||||
TARGET_STATUS_INCLUDES_BY_TYPE = {
|
||||
status: :status,
|
||||
reblog: [status: :reblog],
|
||||
direct: [mention: :status],
|
||||
mention: [mention: :status],
|
||||
favourite: [favourite: :status],
|
||||
poll: [poll: :status],
|
||||
@ -57,6 +60,7 @@ class Notification < ApplicationRecord
|
||||
|
||||
with_options foreign_key: 'activity_id', optional: true do
|
||||
belongs_to :mention, inverse_of: :notification
|
||||
belongs_to :direct, inverse_of: :notification
|
||||
belongs_to :status, inverse_of: :notification
|
||||
belongs_to :follow, inverse_of: :notification
|
||||
belongs_to :follow_request, inverse_of: :notification
|
||||
@ -83,6 +87,8 @@ class Notification < ApplicationRecord
|
||||
favourite&.status
|
||||
when :mention
|
||||
mention&.status
|
||||
when :direct
|
||||
mention&.status
|
||||
when :poll
|
||||
poll&.status
|
||||
end
|
||||
@ -130,7 +136,7 @@ class Notification < ApplicationRecord
|
||||
notification.status.reblog = cached_status
|
||||
when :favourite
|
||||
notification.favourite.status = cached_status
|
||||
when :mention
|
||||
when :mention, :direct
|
||||
notification.mention.status = cached_status
|
||||
when :poll
|
||||
notification.poll.status = cached_status
|
||||
@ -152,7 +158,7 @@ class Notification < ApplicationRecord
|
||||
case activity_type
|
||||
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report'
|
||||
self.from_account_id = activity&.account_id
|
||||
when 'Mention'
|
||||
when 'Mention', 'Direct'
|
||||
self.from_account_id = activity&.status&.account_id
|
||||
when 'Account'
|
||||
self.from_account_id = activity&.id
|
||||
|
@ -61,7 +61,7 @@ class PublicFeed
|
||||
end
|
||||
|
||||
def public_scope
|
||||
Status.with_public_visibility.joins(:account).merge(Account.without_suspended.without_silenced)
|
||||
Status.with_unlisted_visibility.joins(:account).merge(Account.without_suspended.without_silenced)
|
||||
end
|
||||
|
||||
def local_only_scope
|
||||
|
@ -104,6 +104,9 @@ class Status < ApplicationRecord
|
||||
scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
|
||||
scope :without_reblogs, -> { where(statuses: { reblog_of_id: nil }) }
|
||||
scope :with_public_visibility, -> { where(visibility: :public) }
|
||||
scope :with_unlisted_visibility, -> { where(visibility: :unlisted) }
|
||||
scope :without_direct_visibility, -> { where(visibility: [:unlisted ,:public,:private]) }
|
||||
scope :with_direct_visibility, -> { where(visibility: :direct) }
|
||||
scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) }
|
||||
scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) }
|
||||
scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) }
|
||||
|
@ -9,7 +9,7 @@ class StatusPolicy < ApplicationPolicy
|
||||
|
||||
def show?
|
||||
return false if author.suspended?
|
||||
|
||||
return true if role.can?(:manage_users, :manage_reports)
|
||||
if requires_mention?
|
||||
owned? || mention_exists?
|
||||
elsif private?
|
||||
|
@ -12,7 +12,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
||||
end
|
||||
|
||||
def status_type?
|
||||
[:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type)
|
||||
[:favourite, :reblog, :status, :mention, :poll, :update, :direct].include?(object.type)
|
||||
end
|
||||
|
||||
def report_type?
|
||||
|
@ -36,19 +36,18 @@ class FanOutOnWriteService < BaseService
|
||||
end
|
||||
|
||||
def fan_out_to_local_recipients!
|
||||
deliver_to_self!
|
||||
notify_mentioned_accounts!
|
||||
|
||||
notify_about_update! if update?
|
||||
|
||||
case @status.visibility.to_sym
|
||||
when :public, :unlisted, :private
|
||||
notify_mentioned_accounts!
|
||||
deliver_to_self!
|
||||
deliver_to_all_followers!
|
||||
deliver_to_lists!
|
||||
when :limited
|
||||
deliver_to_mentioned_followers!
|
||||
else
|
||||
deliver_to_mentioned_followers!
|
||||
deliver_to_conversation!
|
||||
notify_direct_accounts!
|
||||
end
|
||||
end
|
||||
|
||||
@ -73,6 +72,14 @@ class FanOutOnWriteService < BaseService
|
||||
end
|
||||
end
|
||||
|
||||
def notify_direct_accounts!
|
||||
@status.active_mentions.where.not(id: @options[:silenced_account_ids] || []).joins(:account).merge(Account.local).select(:id, :account_id).reorder(nil).find_in_batches do |mentions|
|
||||
LocalNotificationWorker.push_bulk(mentions) do |mention|
|
||||
[mention.account_id, mention.id, 'Mention', 'direct']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def notify_about_update!
|
||||
@status.reblogged_by_accounts.merge(Account.local).select(:id).reorder(nil).find_in_batches do |accounts|
|
||||
LocalNotificationWorker.push_bulk(accounts) do |account|
|
||||
|
@ -52,7 +52,7 @@ class NotifyService < BaseService
|
||||
end
|
||||
|
||||
def message?
|
||||
@notification.type == :mention
|
||||
@notification.type == :mention || @notification.type == :direct
|
||||
end
|
||||
|
||||
def direct_message?
|
||||
|
@ -11,7 +11,7 @@
|
||||
- if account.suspended? || account.user_pending?
|
||||
\-
|
||||
- else
|
||||
= friendly_number_to_human account.statuses_count
|
||||
= number_with_delimiter account.statuses_count
|
||||
%small= t('accounts.posts', count: account.statuses_count).downcase
|
||||
%td.accounts-table__count.optional
|
||||
- if account.suspended? || account.user_pending?
|
||||
|
@ -7,8 +7,9 @@
|
||||
.filter-subset
|
||||
%strong= t('admin.statuses.media.title')
|
||||
%ul
|
||||
%li= filter_link_to t('generic.all'), media: nil, id: nil
|
||||
%li= filter_link_to t('admin.statuses.with_media'), media: '1'
|
||||
%li= filter_link_to t('generic.all'), media: nil, id: nil, direct: nil
|
||||
%li= filter_link_to t('admin.statuses.with_media'), media: '1', direct: nil
|
||||
%li= filter_link_to t('admin.statuses.with_direct'), media: nil, direct: '1'
|
||||
.back-link
|
||||
- if params[:report_id]
|
||||
= link_to admin_report_path(params[:report_id].to_i) do
|
||||
|
@ -805,6 +805,7 @@ ko:
|
||||
trending: 유행 중
|
||||
visibility: 공개 설정
|
||||
with_media: 미디어 있음
|
||||
with_direct: DM 보기
|
||||
strikes:
|
||||
actions:
|
||||
delete_statuses: "%{name} 님이 %{target} 님의 게시물을 삭제했습니다"
|
||||
|
@ -21,10 +21,10 @@ Rails.application.routes.draw do
|
||||
/publish
|
||||
/follow_requests
|
||||
/direct_messages
|
||||
/local
|
||||
/blocks
|
||||
/domain_blocks
|
||||
/mutes
|
||||
/public/local
|
||||
/followed_tags
|
||||
/statuses/(*any)
|
||||
/deck/(*any)
|
||||
|
Loading…
Reference in New Issue
Block a user