1
0

Merge branch 'heads/origin' into base

This commit is contained in:
オスカー、 2024-04-07 19:31:00 +09:00
commit a081a02c83
41 changed files with 165 additions and 1125 deletions

View File

@ -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)

View File

@ -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')) {
@ -126,7 +126,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
}
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
const excludeTypesFromFilter = filter => {
const allTypes = ImmutableList([
'follow',
@ -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 = () => {};
@ -315,4 +315,4 @@ export function setBrowserPermission (value) {
type: NOTIFICATIONS_SET_BROWSER_PERMISSION,
value,
};
}
}

View File

@ -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}`, {

View File

@ -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;

View File

@ -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) {

View File

@ -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));

View File

@ -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));

View File

@ -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) {

View File

@ -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' });

View File

@ -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}>

View File

@ -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

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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));

View File

@ -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;

View File

@ -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 && (

View File

@ -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);

View File

@ -19,8 +19,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;

View File

@ -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} /> */}

View File

@ -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');
}

View File

@ -58,7 +58,7 @@ const initialState = ImmutableMap({
quickFilter: ImmutableMap({
active: 'all',
show: true,
advanced: true,
advanced: false,
}),
dismissPermissionBanner: false,

View File

@ -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];
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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
@ -122,6 +134,14 @@ class AccountStatusesFilter
def pinned?
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)

View File

@ -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

View File

@ -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

View File

@ -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
@ -78,11 +82,13 @@ class Notification < ApplicationRecord
when :status, :update
status
when :reblog
status&.reblog
status&.reblog
when :favourite
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

View File

@ -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

View File

@ -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 }) }

View File

@ -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?

View File

@ -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?

View File

@ -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|

View File

@ -52,7 +52,7 @@ class NotifyService < BaseService
end
def message?
@notification.type == :mention
@notification.type == :mention || @notification.type == :direct
end
def direct_message?

View File

@ -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?

View File

@ -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

View File

@ -805,6 +805,7 @@ ko:
trending: 유행 중
visibility: 공개 설정
with_media: 미디어 있음
with_direct: DM 보기
strikes:
actions:
delete_statuses: "%{name} 님이 %{target} 님의 게시물을 삭제했습니다"

View File

@ -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)