1
0

add local only timeline

This commit is contained in:
whippyshou 2023-11-03 06:01:48 +09:00
parent 96a814d31e
commit 25cc000a97
21 changed files with 397 additions and 168 deletions

View File

@ -142,7 +142,7 @@ const excludeTypesFromFilter = filter => {
'admin.report',
]);
return allTypes.filterNot(item => filter === 'direct'? item ==='mention' :item === filter ).toJS();
return allTypes.filterNot(item => filter === 'direct'? item ==='mention':item === filter ).toJS();
};
const noOp = () => {};

View File

@ -0,0 +1,44 @@
/* 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

@ -0,0 +1,14 @@
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

@ -0,0 +1,195 @@
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.home' 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

@ -1,74 +0,0 @@
import { connect } from 'react-redux';
import { initBoostModal } from '../../../actions/boosts';
import { mentionCompose } from '../../../actions/compose';
import {
reblog,
favourite,
unreblog,
unfavourite,
} from '../../../actions/interactions';
import {
hideStatus,
revealStatus,
} from '../../../actions/statuses';
import { boostModal } from '../../../initial_state';
import { makeGetNotification, makeGetStatus, makeGetReport } from '../../../selectors';
import Notification from '../components/notification';
const makeMapStateToProps = () => {
const getNotification = makeGetNotification();
const getStatus = makeGetStatus();
const getReport = makeGetReport();
const mapStateToProps = (state, props) => {
const notification = getNotification(state, props.notification, props.accountId);
return {
notification: notification ,
status: notification.get('status') ? getStatus(state, { id: notification.get('status'), contextType: 'notifications' }) : null,
report: notification.get('report') ? getReport(state, notification.get('report'), notification.getIn(['report', 'target_account', 'id'])) : null,
};
};
return mapStateToProps;
};
const mapDispatchToProps = dispatch => ({
onMention: (account, router) => {
dispatch(mentionCompose(account, router));
},
onModalReblog (status, privacy) {
dispatch(reblog(status, privacy));
},
onReblog (status, e) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
if (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));
}
},
onToggleHidden (status) {
if (status.get('hidden')) {
dispatch(revealStatus(status.get('id')));
} else {
dispatch(hideStatus(status.get('id')));
}
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(Notification);

View File

@ -1,74 +0,0 @@
import { connect } from 'react-redux';
import { initBoostModal } from '../../../actions/boosts';
import { mentionCompose } from '../../../actions/compose';
import {
reblog,
favourite,
unreblog,
unfavourite,
} from '../../../actions/interactions';
import {
hideStatus,
revealStatus,
} from '../../../actions/statuses';
import { boostModal } from '../../../initial_state';
import { makeGetNotification, makeGetStatus, makeGetReport } from '../../../selectors';
import Notification from '../components/notification';
const makeMapStateToProps = () => {
const getNotification = makeGetNotification();
const getStatus = makeGetStatus();
const getReport = makeGetReport();
const mapStateToProps = (state, props) => {
const notification = getNotification(state, props.notification, props.accountId);
return {
notification: (notification.get('status') && getStatus(state, { id: notification.get('status'), contextType: 'notifications' }).get('visibility')!=='direct') || notification.get('report') ? notification : null,
status: notification.get('status') ? getStatus(state, { id: notification.get('status'), contextType: 'notifications' }) : null,
report: notification.get('report') ? getReport(state, notification.get('report'), notification.getIn(['report', 'target_account', 'id'])) : null,
};
};
return mapStateToProps;
};
const mapDispatchToProps = dispatch => ({
onMention: (account, router) => {
dispatch(mentionCompose(account, router));
},
onModalReblog (status, privacy) {
dispatch(reblog(status, privacy));
},
onReblog (status, e) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
if (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));
}
},
onToggleHidden (status) {
if (status.get('hidden')) {
dispatch(revealStatus(status.get('id')));
} else {
dispatch(hideStatus(status.get('id')));
}
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(Notification);

View File

@ -45,9 +45,9 @@ class StatusCheckBox extends PureComponent {
const visibilityIconInfo = {
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
'unlisted': { icon: 'cloud', text: intl.formatMessage(messages.unlisted_short) },
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
'direct': { icon: 'explore', text: intl.formatMessage(messages.direct_short) },
};
const visibilityIcon = visibilityIconInfo[status.get('visibility')];

View File

@ -76,7 +76,7 @@ class NavigationPanel extends Component {
</>
)}
{(signedIn) && (
<ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='hashtag' text={intl.formatMessage(messages.firehose)} />
<ColumnLink transparent to='/local' isActive={this.isFirehoseActive} icon='hashtag' text={intl.formatMessage(messages.firehose)} />
)}
{!signedIn && (

View File

@ -0,0 +1,66 @@
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

@ -12,7 +12,8 @@ const makeGetStatusIds = (pending = false) => createSelector([
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
(state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()),
(state) => state.get('statuses'),
], (columnSettings, statusIds, statuses) => {
(type) => type
], (columnSettings, statusIds, statuses,type) => {
return statusIds.filter(id => {
if (id === null) return true;

View File

@ -40,6 +40,7 @@ import {
AccountTimeline,
AccountGallery,
HomeTimeline,
LocalTimeline,
Followers,
Following,
Reblogs,
@ -199,7 +200,12 @@ class SwitchingColumnsArea extends PureComponent {
<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} />
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} 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='/tags/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />

View File

@ -14,6 +14,9 @@ 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');
}

View File

@ -117,6 +117,7 @@
"column.firehose": "Live feeds",
"column.follow_requests": "Follow requests",
"column.home": "Home",
"column.local": "feed",
"column.lists": "Lists",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
@ -239,6 +240,7 @@
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up.",
"empty_column.local": "Your home timeline is empty! Follow more people to fill it up.",
"empty_column.list": "There is nothing in this list yet. When members of this list publish new posts, they will appear here.",
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
"empty_column.mutes": "You haven't muted any users yet.",
@ -309,8 +311,14 @@
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"local.column_settings.basic": "local",
"local.column_settings.show_media": "Show Media",
"home.explore_prompt.body": "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:",
"home.explore_prompt.title": "This is your home base within Mastodon.",
"local.explore_prompt.body": "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:",
"local.explore_prompt.title": "This is your home base within Mastodon.",
"home.hide_announcements": "Hide announcements",
"home.pending_critical_update.body": "Please update your Mastodon server as soon as possible!",
"home.pending_critical_update.link": "See updates",

View File

@ -117,6 +117,7 @@
"column.firehose": "실시간 피드",
"column.follow_requests": "팔로우 요청",
"column.home": "홈",
"column.local": "실시간 피드",
"column.lists": "리스트",
"column.mutes": "뮤트한 사용자",
"column.notifications": "알림",
@ -239,6 +240,7 @@
"empty_column.followed_tags": "아직 아무 해시태그도 팔로우하고 있지 않습니다. 해시태그를 팔로우하면, 여기에 표시됩니다.",
"empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.",
"empty_column.home": "당신의 홈 타임라인은 비어있습니다! 더 많은 사람들을 팔로우 하여 채워보세요.",
"empty_column.local": "당신의 실시간 피드는 비어있습니다! 더 많은 사람들을 팔로우 하여 채워보세요.",
"empty_column.list": "리스트에 아직 아무것도 없습니다. 리스트의 누군가가 게시물을 올리면 여기에 나타납니다.",
"empty_column.lists": "아직 리스트가 없습니다. 리스트를 만들면 여기에 나타납니다.",
"empty_column.mutes": "아직 아무도 뮤트하지 않았습니다.",
@ -309,8 +311,14 @@
"home.column_settings.basic": "기본",
"home.column_settings.show_reblogs": "부스트 표시",
"home.column_settings.show_replies": "답글 표시",
"local.column_settings.basic": "실시간 피드",
"local.column_settings.show_media": "미디어만",
"home.explore_prompt.body": "홈 피드에는 내가 팔로우한 해시태그 그리고 팔로우한 사람과 부스트가 함께 나타납니다. 너무 고요하게 느껴진다면, 다음 것들을 살펴볼 수 있습니다.",
"home.explore_prompt.title": "이곳은 마스토돈의 내 본거지입니다.",
"local.explore_prompt.body": "실시간 피드에는 {title}에 있는 사람들의 답장이 아닌 최근 게시글을 살펴볼 수 있습니다.",
"local.explore_prompt.title": "이곳은 마스토돈의 내 본거지입니다.",
"home.hide_announcements": "공지사항 숨기기",
"home.pending_critical_update.body": "서둘러 마스토돈 서버를 업데이트 하세요!",
"home.pending_critical_update.link": "업데이트 보기",

View File

@ -20,7 +20,7 @@ const initialState = ImmutableMap({
home: ImmutableMap({
shows: ImmutableMap({
reblog: true,
media: true,
reply: true,
}),
@ -29,6 +29,17 @@ const initialState = ImmutableMap({
}),
}),
local: ImmutableMap({
shows: ImmutableMap({
media: false
}),
regex: ImmutableMap({
body: '',
}),
}),
notifications: ImmutableMap({
alerts: ImmutableMap({
follow: false,

View File

@ -1551,21 +1551,21 @@ body.embed .detailed-status__favorites {
body.embed .detailed-status__link > .fa-reply + span::after,
.layout-multiple-columns .detailed-status__link > .fa-reply + span::after {
color: var(--color-dim);
content: 'Replies';
content: '답장';
font-weight: var(--font-weight-semibold);
}
body.embed .detailed-status__link > .fa-retweet + span::after,
.layout-multiple-columns .detailed-status__link > .fa-retweet + span::after {
color: var(--color-dim);
content: 'Boosts';
content: '부스트';
font-weight: var(--font-weight-semibold);
}
body.embed .detailed-status__link > .fa-star + span::after,
.layout-multiple-columns .detailed-status__link > .fa-star + span::after {
color: var(--color-dim);
content: 'Favourites';
content: '좋아요';
font-weight: var(--font-weight-semibold);
}
@ -2961,6 +2961,11 @@ body.embed .button.logo-button:hover,
order: 5;
}
.layout-multiple-columns .column-link[href="/local"] {
order: 5;
}
.layout-multiple-columns .column-link[href="/public"] {
order: 6;
}

View File

@ -1584,21 +1584,21 @@ body.embed > .activity-stream {
body.embed .detailed-status__link > .fa-reply + span::after,
.layout-single-column .detailed-status__link > .fa-reply + span::after {
color: var(--color-dim);
content: 'Replies';
content: '답장';
font-weight: var(--font-weight-semibold);
}
body.embed .detailed-status__link > .fa-retweet + span::after,
.layout-single-column .detailed-status__link > .fa-retweet + span::after {
color: var(--color-dim);
content: 'Boosts';
content: '부스트';
font-weight: var(--font-weight-semibold);
}
body.embed .detailed-status__link > .fa-star + span::after,
.layout-single-column .detailed-status__link > .fa-star + span::after {
color: var(--color-dim);
content: 'Favourites';
content: '좋아요';
font-weight: var(--font-weight-semibold);
}
@ -2997,6 +2997,10 @@ body.embed .button.logo-button:hover,
order: 5;
}
.layout-single-column .column-link[href="/local"] {
order: 5;
}
.layout-single-column .column-link[href="/public"] {
order: 6;
}

View File

@ -1551,21 +1551,21 @@ body.embed .detailed-status__favorites {
body.embed .detailed-status__link > .fa-reply + span::after,
.layout-multiple-columns .detailed-status__link > .fa-reply + span::after {
color: var(--color-dim);
content: 'Replies';
content: '답장';
font-weight: var(--font-weight-semibold);
}
body.embed .detailed-status__link > .fa-retweet + span::after,
.layout-multiple-columns .detailed-status__link > .fa-retweet + span::after {
color: var(--color-dim);
content: 'Boosts';
content: '부스트';
font-weight: var(--font-weight-semibold);
}
body.embed .detailed-status__link > .fa-star + span::after,
.layout-multiple-columns .detailed-status__link > .fa-star + span::after {
color: var(--color-dim);
content: 'Favourites';
content: '좋아요';
font-weight: var(--font-weight-semibold);
}
@ -2961,6 +2961,12 @@ body.embed .button.logo-button:hover,
order: 5;
}
.layout-multiple-columns .column-link[href="/local"] {
order: 5;
}
.layout-multiple-columns .column-link[href="/public"] {
order: 6;
}

View File

@ -1590,21 +1590,21 @@ body.embed > .activity-stream {
body.embed .detailed-status__link > .fa-reply + span::after,
.layout-single-column .detailed-status__link > .fa-reply + span::after {
color: var(--color-dim);
content: 'Replies';
content: '답장';
font-weight: var(--font-weight-semibold);
}
body.embed .detailed-status__link > .fa-retweet + span::after,
.layout-single-column .detailed-status__link > .fa-retweet + span::after {
color: var(--color-dim);
content: 'Boosts';
content: '부스트';
font-weight: var(--font-weight-semibold);
}
body.embed .detailed-status__link > .fa-star + span::after,
.layout-single-column .detailed-status__link > .fa-star + span::after {
color: var(--color-dim);
content: 'Favourites';
content: '좋아요';
font-weight: var(--font-weight-semibold);
}
@ -3002,6 +3002,10 @@ body.embed .button.logo-button:hover,
order: 5;
}
.layout-single-column .column-link[href="/local"] {
order: 5;
}
.layout-single-column .column-link[href="/public"] {
order: 6;
}

View File

@ -25,6 +25,7 @@
%p.hint= t 'appearance.advanced_web_interface_hint'
.fields-group
= ff.input :'web.advanced_layout', wrapper: :with_label, hint: false, label: I18n.t('simple_form.labels.defaults.setting_advanced_layout')
%h4= t 'appearance.animations_and_accessibility'
.fields-group

View File

@ -23,6 +23,7 @@ Rails.application.routes.draw do
/publish
/follow_requests
/direct_messages
/local
/blocks
/domain_blocks
/mutes