1
0
mirror of https://github.com/whippyshou/mastodon synced 2024-11-30 15:58:20 +09:00
This commit is contained in:
whippyshou 2024-03-27 22:03:47 +09:00
parent bceb739e2a
commit d7057726fb
40 changed files with 165 additions and 1125 deletions

View File

@ -148,6 +148,7 @@ GEM
net-http-persistent (~> 4.0) net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8) nokogiri (~> 1, >= 1.10.8)
base64 (0.1.1) base64 (0.1.1)
bcp47_spec (0.2.1)
bcrypt (3.1.18) bcrypt (3.1.18)
better_errors (2.10.1) better_errors (2.10.1)
erubi (>= 1.0.0) erubi (>= 1.0.0)
@ -377,19 +378,19 @@ GEM
ipaddress (0.8.3) ipaddress (0.8.3)
jmespath (1.6.2) jmespath (1.6.2)
json (2.6.3) json (2.6.3)
json-canonicalization (0.3.2) json-canonicalization (1.0.0)
json-jwt (1.15.3) json-jwt (1.15.3)
activesupport (>= 4.2) activesupport (>= 4.2)
aes_key_wrap aes_key_wrap
bindata bindata
httpclient httpclient
json-ld (3.2.5) json-ld (3.3.1)
htmlentities (~> 4.3) htmlentities (~> 4.3)
json-canonicalization (~> 0.3, >= 0.3.2) json-canonicalization (~> 1.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
multi_json (~> 1.15) multi_json (~> 1.15)
rack (>= 2.2, < 4) rack (>= 2.2, < 4)
rdf (~> 3.2, >= 3.2.10) rdf (~> 3.3)
json-ld-preloaded (3.2.2) json-ld-preloaded (3.2.2)
json-ld (~> 3.2) json-ld (~> 3.2)
rdf (~> 3.2) rdf (~> 3.2)
@ -593,7 +594,8 @@ GEM
zeitwerk (~> 2.5) zeitwerk (~> 2.5)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.0.6) rake (13.0.6)
rdf (3.2.11) rdf (3.3.1)
bcp47_spec (~> 0.2)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.6.1) rdf-normalize (0.6.1)
rdf (~> 3.2) rdf (~> 3.2)

View File

@ -62,13 +62,13 @@ export const loadPending = () => ({
export function updateNotifications(notification, intlMessages, intlLocale) { export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => { return (dispatch, getState) => {
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); 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 showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === 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 showAlert = 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 playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
let filtered = false; 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')); const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications'));
if (filters.some(result => result.filter.filter_action === 'hide')) { if (filters.some(result => result.filter.filter_action === 'hide')) {
@ -142,7 +142,7 @@ const excludeTypesFromFilter = filter => {
'admin.report', 'admin.report',
]); ]);
return allTypes.filterNot(item => filter === 'direct'? item ==='mention':item === filter ).toJS(); return allTypes.filterNot(item => item === filter).toJS();
}; };
const noOp = () => {}; const noOp = () => {};

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 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 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 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 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, exclude_reblogs: true, 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 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 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) => { export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, { return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {

View File

@ -251,11 +251,11 @@ class Status extends ImmutablePureComponent {
handleStatusClick = (e) => { handleStatusClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) { // if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
e.preventDefault(); // e.preventDefault();
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`+`/${this.props.status.get('id')}`) // this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`+`/${this.props.status.get('id')}`)
} // }
e.stopPropagation(); // e.stopPropagation();
}; };
handleDeployPictureInPicture = (type, mediaProps) => { handleDeployPictureInPicture = (type, mediaProps) => {
const { deployPictureInPicture } = this.props; const { deployPictureInPicture } = this.props;

View File

@ -8,9 +8,6 @@ import { debounce } from 'lodash';
import RegenerationIndicator from 'mastodon/components/regeneration_indicator'; import RegenerationIndicator from 'mastodon/components/regeneration_indicator';
import StatusContainer from '../containers/status_container'; 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 { LoadGap } from './load_gap';
import ScrollableList from './scrollable_list'; import ScrollableList from './scrollable_list';
@ -101,34 +98,7 @@ export default class StatusList extends ImmutablePureComponent {
maxId={index > 0 ? statusIds.get(index - 1) : null} maxId={index > 0 ? statusIds.get(index - 1) : null}
onClick={onLoadMore} 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 <StatusContainer
key={statusId} key={statusId}
id={statusId} id={statusId}
@ -138,7 +108,8 @@ export default class StatusList extends ImmutablePureComponent {
scrollKey={this.props.scrollKey} scrollKey={this.props.scrollKey}
showThread showThread
withCounters={this.props.withCounters} withCounters={this.props.withCounters}
/>)) />
))
) : null; ) : null;
if (scrollableContent && featuredStatusIds) { 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 () { componentDidMount () {
const { params: { acct }, accountId, dispatch } = this.props; const { params: { acct }, accountId, dispatch } = this.props;
if (accountId) { if (accountId) {
this._load(); this._load();
} else { } else {
@ -111,7 +110,6 @@ class AccountTimeline extends ImmutablePureComponent {
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
const { params: { acct }, accountId, dispatch } = this.props; const { params: { acct }, accountId, dispatch } = this.props;
if (prevProps.accountId !== accountId && accountId) { if (prevProps.accountId !== accountId && accountId) {
this._load(); this._load();
} else if (prevProps.params.acct !== acct) { } else if (prevProps.params.acct !== acct) {

View File

@ -4,7 +4,6 @@ import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
const messages = defineMessages({ const messages = defineMessages({
@ -38,14 +37,14 @@ class ActionBar extends PureComponent {
render () { render () {
const { intl } = this.props; const { intl } = this.props;
const username = this.props.account.get('acct')
let menu = []; let menu = [];
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); 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.preferences), href: '/settings/preferences' });
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
menu.push(null); 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(null);
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); 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 { value, container, disabled, intl } = this.props;
const { open, placement } = this.state; 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 ( return (
<div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}> <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']), state => state.getIn(['notifications', 'items']),
], (showFilterBar, allowedType, excludedTypes, notifications) => { ], (showFilterBar, allowedType, excludedTypes, notifications) => {
if (!showFilterBar || allowedType === 'all') { 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 || allowedType === item.get('type'));
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'))
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@ -192,7 +190,7 @@ class Notifications extends PureComponent {
}; };
render() { 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 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 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; const { signedIn } = this.context.identity;

View File

@ -75,7 +75,7 @@ class NavigationPanel extends Component {
</> </>
)} )}
{(signedIn) && ( {(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 && ( {!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

@ -20,8 +20,6 @@ const makeGetStatusIds = (pending = false) => createSelector([
const statusForId = statuses.get(id); const statusForId = statuses.get(id);
let showStatus = true; let showStatus = true;
if (statusForId.get('visibility') === 'direct') return false;
if (statusForId.get('account') === me) return true; if (statusForId.get('account') === me) return true;
if (columnSettings.getIn(['shows', 'reblog']) === false) { if (columnSettings.getIn(['shows', 'reblog']) === false) {

View File

@ -62,8 +62,7 @@ import {
Explore, Explore,
Onboarding, Onboarding,
About, About,
PrivacyPolicy, PrivacyPolicy
SendDirectMessagesStatuses
} from './util/async-components'; } from './util/async-components';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; 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} /> <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 /> <Redirect from='/timelines/public/local' to='/public/local' exact />
<WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} /> <WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} 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={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} /> <WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
<WrappedRoute path='/tags/:id' component={HashtagTimeline} 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='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} 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='/start' exact component={Onboarding} content={children} />
{/* <WrappedRoute path='/directory' component={Directory} 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'); return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline');
} }
export function LocalTimeline () {
return import(/* webpackChunkName: "features/home_timeline" */'../../local_timeline');
}
export function PublicTimeline () { export function PublicTimeline () {
return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline'); return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline');
} }
@ -61,10 +58,6 @@ export function PinnedStatuses () {
return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses'); return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses');
} }
export function SendDirectMessagesStatuses () {
return import(/* webpackChunkName: "features/pinned_statuses" */'../../direct_messages');
}
export function AccountDirectMessages () { export function AccountDirectMessages () {
return import(/* webpackChunkName: "features/pinned_statuses" */'../../account_direct'); return import(/* webpackChunkName: "features/pinned_statuses" */'../../account_direct');
} }

View File

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

View File

@ -24,24 +24,23 @@ export type ShortNumber = [number, DecimalUnits, 0 | 1]; // Array of: shorten nu
* // => [5.936, 1000, 1] * // => [5.936, 1000, 1]
*/ */
export function toShortNumber(sourceNumber: number): ShortNumber { export function toShortNumber(sourceNumber: number): ShortNumber {
if (sourceNumber < DECIMAL_UNITS.THOUSAND) { // if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
return [sourceNumber, DECIMAL_UNITS.ONE, 0]; // return [sourceNumber, DECIMAL_UNITS.ONE, 0];
} else if (sourceNumber < DECIMAL_UNITS.MILLION) { // } else if (sourceNumber < DECIMAL_UNITS.MILLION) {
return [ // return [
sourceNumber / DECIMAL_UNITS.THOUSAND, // sourceNumber / DECIMAL_UNITS.THOUSAND,
DECIMAL_UNITS.THOUSAND, // DECIMAL_UNITS.THOUSAND,
sourceNumber < TEN_THOUSAND ? 1 : 0, // sourceNumber < TEN_THOUSAND ? 1 : 0,
]; // ];
} else if (sourceNumber < DECIMAL_UNITS.BILLION) { // } else if (sourceNumber < DECIMAL_UNITS.BILLION) {
return [ // return [
sourceNumber / DECIMAL_UNITS.MILLION, // sourceNumber / DECIMAL_UNITS.MILLION,
DECIMAL_UNITS.MILLION, // DECIMAL_UNITS.MILLION,
sourceNumber < TEN_MILLIONS ? 1 : 0, // sourceNumber < TEN_MILLIONS ? 1 : 0,
]; // ];
} else if (sourceNumber < DECIMAL_UNITS.TRILLION) { // } else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
return [sourceNumber / DECIMAL_UNITS.BILLION, DECIMAL_UNITS.BILLION, 0]; // return [sourceNumber / DECIMAL_UNITS.BILLION, DECIMAL_UNITS.BILLION, 0];
} // }
return [sourceNumber, DECIMAL_UNITS.ONE, 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:#df98a2;
--color-accent-dark: #be2a4d; --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-bg: #fff;
--color-fg: #000; --color-fg: #000;
--color-border: #e9dee0;
--color-dim: #c16c80;
--color-light-text: #1f1b23; --color-light-text: #1f1b23;
--color-green: #17bf63; --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-focusable-toot: rgba(0, 0, 0, 0.035);
--color-light-shade: #db9e9e12; --color-light-shade: #db9e9e12;
--color-mud: #f0dee2;
--color-black-coral: #9188a6; --color-black-coral: #9188a6;
--color-profile-button-hover: #db9e9e40; --color-profile-button-hover: #f1eff41a;
--color-column-link-hover: #db9e9e40; --color-column-link-hover: #f7f7f91a;
--color-modal-overlay: #6a5b8366; --color-modal-overlay: #6a5b8366;
--color-dark: #f7f9f9; --color-dark: #f7f9f9;
--color-thread-line: #e1e8ed; --color-thread-line: #e1e8ed;
--color-gainsboro: #8899a6; --color-gainsboro: #8899a6;
--color-light-purple: #df98a2;
--color-dark-electric-blue: #9088a6; --color-dark-electric-blue: #9088a6;
--color-bg-75: #ffffffbf; --color-bg-75: #ffffffbf;
--color-accent: var(--color-accent-dark); --color-accent: var(--color-accent-dark);
--color-ghost-button-text: 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-hashtag: var(--color-accent-dark);
--color-mention: 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-threaded-line: #434264;
--color-brand-mastodon-text-light: #8493a7; --color-brand-mastodon-text-light: #8493a7;
/* Colors */ /* Colors */
/* Note: Remember to search for the DIM hex /* Note: Remember to search for the DIM hex
and replace it inside the SVG icons if you decide to change it */ 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: #e0245e;
--color-red-75: #e0245ebf; --color-red-75: #e0245ebf;
--color-light-shade: #db9e9e12; --color-light-shade: #00000005;
--color-focusable-toot: rgba(0, 0, 0, 0.035); --color-focusable-toot: rgba(0, 0, 0, 0.035);
--color-light-text: #1f1b23; --color-light-text: #1f1b23;
@ -248,8 +246,8 @@ body.theme-bird-ui-light.layout-single-column {
--color-black-coral: #9188a6; --color-black-coral: #9188a6;
--color-profile-button-hover: #db9e9e40; --color-profile-button-hover: #1e1b231a;
--color-column-link-hover: #db9e9e40; --color-column-link-hover: #1e1b231a;
--color-modal-overlay: #6a5b8366; --color-modal-overlay: #6a5b8366;
--color-dark: #f7f9f9; --color-dark: #f7f9f9;
@ -3014,7 +3012,9 @@ body.embed .button.logo-button:hover,
.layout-single-column .column-link[href="/local"] { .layout-single-column .column-link[href="/local"] {
order: 5; order: 5;
} }
.layout-single-column .column-link[href="/pulic/local"] {
order: 5;
}
.layout-single-column .column-link[href='/follow_requests'] { .layout-single-column .column-link[href='/follow_requests'] {
order: 8; order: 8;
} }

View File

@ -234,13 +234,13 @@ body.theme-default.layout-multiple-columns {
--color-red-75: #e0245ebf; --color-red-75: #e0245ebf;
--color-focusable-toot: rgba(0, 0, 0, 0.035); --color-focusable-toot: rgba(0, 0, 0, 0.035);
--color-light-shade: #db9e9e12; --color-light-shade: #00000005;;
--color-mud: #f0dee2; --color-mud: #f0dee2;
--color-black-coral: #9188a6; --color-black-coral: #9188a6;
--color-profile-button-hover: #db9e9e40; --color-profile-button-hover: #f1eff41a;
--color-column-link-hover: #db9e9e40; --color-column-link-hover: #f7f7f91a;
--color-modal-overlay: #6a5b8366; --color-modal-overlay: #6a5b8366;
--color-dark: #f7f9f9; --color-dark: #f7f9f9;

View File

@ -58,7 +58,7 @@
--color-thread-line: var(--color-brand-mastodon-threaded-line); --color-thread-line: var(--color-brand-mastodon-threaded-line);
--color-topaz: #dadaf3; --color-topaz: #dadaf3;
--color-light-purple: #df98a2; --color-light-purple: #9baec8;
--color-lighter-purple: #a5b8d3; --color-lighter-purple: #a5b8d3;
--color-dark-electric-blue: #576078; --color-dark-electric-blue: #576078;
@ -239,43 +239,43 @@ body.theme-default.layout-single-column {
--color-accent:#df98a2; --color-accent:#df98a2;
--color-accent-dark: #be2a4d; --color-accent-dark: #be2a4d;
--color-bg: #fff;
--color-fg: #000;
--color-border: #e9dee0; --color-border: #e9dee0;
--color-dim: #c16c80; --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-green: #17bf63;
--color-red: #e0245e; --color-red: #e0245e;
--color-red-75: #e0245ebf; --color-red-75: #e0245ebf;
--color-light-shade: #db9e9e12; --color-light-shade: #00000005;;
--color-focusable-toot: rgba(0, 0, 0, 0.035); --color-focusable-toot: rgba(0, 0, 0, 0.035);
--color-light-text: #1f1b23; --color-light-text: #1f1b23;
--color-mud: #f0dee2;
--color-black-coral: #9188a6; --color-black-coral: #9188a6;
--color-profile-button-hover: #db9e9e40; --color-profile-button-hover: #1e1b231a;
--color-column-link-hover: #db9e9e40; --color-column-link-hover: #1e1b231a;
--color-modal-overlay: #6a5b8366; --color-modal-overlay: #6a5b8366;
--color-dark: #f7f9f9; --color-dark: #f7f9f9;
--color-thread-line: #e1e8ed; --color-thread-line: #e1e8ed;
--color-topaz: #8899a6; --color-topaz: #8899a6;
--color-light-purple: #df98a2;
--color-dark-electric-blue: #9088a6; --color-dark-electric-blue: #9088a6;
--color-bg-75: #ffffffbf; --color-bg-75: #ffffffbf;
--color-accent: var(--color-accent-dark); --color-accent: var(--color-accent-dark);
--color-ghost-button-text: 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-hashtag: var(--color-accent-dark);
--color-mention: 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"] { .layout-single-column .column-link[href="/local"] {
order: 5; order: 5;
} }
.layout-single-column .column-link[href="/public/local"] {
order: 5;
}
.layout-single-column .column-link[href='/follow_requests'] { .layout-single-column .column-link[href='/follow_requests'] {
order: 8; order: 8;

View File

@ -7,6 +7,8 @@ class AccountStatusesFilter
only_media only_media
exclude_replies exclude_replies
exclude_reblogs exclude_reblogs
only_direct
no_direct
).freeze ).freeze
attr_reader :params, :account, :current_account attr_reader :params, :account, :current_account
@ -20,6 +22,8 @@ class AccountStatusesFilter
def results def results
scope = initial_scope 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!(pinned_scope) if pinned?
scope.merge!(only_media_scope) if only_media? scope.merge!(only_media_scope) if only_media?
scope.merge!(no_replies_scope) if exclude_replies? scope.merge!(no_replies_scope) if exclude_replies?
@ -69,6 +73,14 @@ class AccountStatusesFilter
) )
end end
def direct_scope
Status.with_direct_visibility
end
def no_direct_scope
Status.without_direct_visibility
end
def only_media_scope def only_media_scope
Status.joins(:media_attachments).merge(account.media_attachments.reorder(nil)).group(Status.arel_table[:id]) Status.joins(:media_attachments).merge(account.media_attachments.reorder(nil)).group(Status.arel_table[:id])
end end
@ -123,6 +135,14 @@ class AccountStatusesFilter
truthy_param?(:pinned) truthy_param?(:pinned)
end end
def only_direct?
truthy_param?(:only_direct)
end
def no_direct?
truthy_param?(:no_direct)
end
def only_media? def only_media?
truthy_param?(:only_media) truthy_param?(:only_media)
end end

View File

@ -3,6 +3,7 @@
class Admin::StatusFilter class Admin::StatusFilter
KEYS = %i( KEYS = %i(
media media
direct
report_id report_id
).freeze ).freeze
@ -33,6 +34,8 @@ class Admin::StatusFilter
case key.to_s case key.to_s
when 'media' when 'media'
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id).reorder('statuses.id desc') 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 else
raise Mastodon::InvalidParameterError, "Unknown filter: #{key}" raise Mastodon::InvalidParameterError, "Unknown filter: #{key}"
end 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) unhydrated = redis.zrangebyscore(key, "(#{min_id}", "(#{max_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
end end
Status.where(id: unhydrated).cache_ids Status.without_direct_visibility.where(id: unhydrated).cache_ids
end end
def key def key

View File

@ -20,6 +20,7 @@ class Notification < ApplicationRecord
include Paginable include Paginable
LEGACY_TYPE_CLASS_MAP = { LEGACY_TYPE_CLASS_MAP = {
'Direct' => :direct,
'Mention' => :mention, 'Mention' => :mention,
'Status' => :reblog, 'Status' => :reblog,
'Follow' => :follow, 'Follow' => :follow,
@ -29,6 +30,7 @@ class Notification < ApplicationRecord
}.freeze }.freeze
TYPES = %i( TYPES = %i(
direct
mention mention
status status
reblog reblog
@ -44,6 +46,7 @@ class Notification < ApplicationRecord
TARGET_STATUS_INCLUDES_BY_TYPE = { TARGET_STATUS_INCLUDES_BY_TYPE = {
status: :status, status: :status,
reblog: [status: :reblog], reblog: [status: :reblog],
direct: [mention: :status],
mention: [mention: :status], mention: [mention: :status],
favourite: [favourite: :status], favourite: [favourite: :status],
poll: [poll: :status], poll: [poll: :status],
@ -57,6 +60,7 @@ class Notification < ApplicationRecord
with_options foreign_key: 'activity_id', optional: true do with_options foreign_key: 'activity_id', optional: true do
belongs_to :mention, inverse_of: :notification belongs_to :mention, inverse_of: :notification
belongs_to :direct, inverse_of: :notification
belongs_to :status, inverse_of: :notification belongs_to :status, inverse_of: :notification
belongs_to :follow, inverse_of: :notification belongs_to :follow, inverse_of: :notification
belongs_to :follow_request, inverse_of: :notification belongs_to :follow_request, inverse_of: :notification
@ -83,6 +87,8 @@ class Notification < ApplicationRecord
favourite&.status favourite&.status
when :mention when :mention
mention&.status mention&.status
when :direct
mention&.status
when :poll when :poll
poll&.status poll&.status
end end
@ -130,7 +136,7 @@ class Notification < ApplicationRecord
notification.status.reblog = cached_status notification.status.reblog = cached_status
when :favourite when :favourite
notification.favourite.status = cached_status notification.favourite.status = cached_status
when :mention when :mention, :direct
notification.mention.status = cached_status notification.mention.status = cached_status
when :poll when :poll
notification.poll.status = cached_status notification.poll.status = cached_status
@ -152,7 +158,7 @@ class Notification < ApplicationRecord
case activity_type case activity_type
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report' when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report'
self.from_account_id = activity&.account_id self.from_account_id = activity&.account_id
when 'Mention' when 'Mention', 'Direct'
self.from_account_id = activity&.status&.account_id self.from_account_id = activity&.status&.account_id
when 'Account' when 'Account'
self.from_account_id = activity&.id self.from_account_id = activity&.id

View File

@ -61,7 +61,7 @@ class PublicFeed
end end
def public_scope 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 end
def local_only_scope 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_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 :without_reblogs, -> { where(statuses: { reblog_of_id: nil }) }
scope :with_public_visibility, -> { where(visibility: :public) } 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 :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 :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 }) } scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) }

View File

@ -12,7 +12,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
end end
def status_type? def status_type?
[:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type) [:favourite, :reblog, :status, :mention, :poll, :update, :direct].include?(object.type)
end end
def report_type? def report_type?

View File

@ -36,19 +36,18 @@ class FanOutOnWriteService < BaseService
end end
def fan_out_to_local_recipients! def fan_out_to_local_recipients!
deliver_to_self!
notify_mentioned_accounts!
notify_about_update! if update? notify_about_update! if update?
case @status.visibility.to_sym case @status.visibility.to_sym
when :public, :unlisted, :private when :public, :unlisted, :private
notify_mentioned_accounts!
deliver_to_self!
deliver_to_all_followers! deliver_to_all_followers!
deliver_to_lists! deliver_to_lists!
when :limited
deliver_to_mentioned_followers!
else else
deliver_to_mentioned_followers!
deliver_to_conversation! deliver_to_conversation!
notify_direct_accounts!
end end
end end
@ -73,6 +72,14 @@ class FanOutOnWriteService < BaseService
end end
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! def notify_about_update!
@status.reblogged_by_accounts.merge(Account.local).select(:id).reorder(nil).find_in_batches do |accounts| @status.reblogged_by_accounts.merge(Account.local).select(:id).reorder(nil).find_in_batches do |accounts|
LocalNotificationWorker.push_bulk(accounts) do |account| LocalNotificationWorker.push_bulk(accounts) do |account|

View File

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

View File

@ -11,7 +11,7 @@
- if account.suspended? || account.user_pending? - if account.suspended? || account.user_pending?
\- \-
- else - else
= friendly_number_to_human account.statuses_count = number_with_delimiter account.statuses_count
%small= t('accounts.posts', count: account.statuses_count).downcase %small= t('accounts.posts', count: account.statuses_count).downcase
%td.accounts-table__count.optional %td.accounts-table__count.optional
- if account.suspended? || account.user_pending? - if account.suspended? || account.user_pending?

View File

@ -7,8 +7,9 @@
.filter-subset .filter-subset
%strong= t('admin.statuses.media.title') %strong= t('admin.statuses.media.title')
%ul %ul
%li= filter_link_to t('generic.all'), media: nil, id: nil %li= filter_link_to t('generic.all'), media: nil, id: nil, direct: nil
%li= filter_link_to t('admin.statuses.with_media'), media: '1' %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 .back-link
- if params[:report_id] - if params[:report_id]
= link_to admin_report_path(params[:report_id].to_i) do = link_to admin_report_path(params[:report_id].to_i) do

View File

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

View File

@ -21,10 +21,10 @@ Rails.application.routes.draw do
/publish /publish
/follow_requests /follow_requests
/direct_messages /direct_messages
/local
/blocks /blocks
/domain_blocks /domain_blocks
/mutes /mutes
/public/local
/followed_tags /followed_tags
/statuses/(*any) /statuses/(*any)
/deck/(*any) /deck/(*any)