diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index ae6dbcb8b378ee..5785e204ed289d 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -29,7 +29,8 @@ def home_statuses limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id], - params[:min_id] + params[:min_id], + visibilities ) end @@ -60,4 +61,10 @@ def pagination_max_id def pagination_since_id @statuses.first.id end + + def visibilities + val = params.permit(visibilities: [])[:visibilities] || [] + val = [val] unless val.is_a?(Enumerable) + val + end end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 32b5d79487b741..cb7f08bc76fa5b 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -55,6 +55,17 @@ def user_settings_params :setting_use_pending_items, :setting_trends, :setting_crop_images, + :setting_show_follow_button_on_timeline, + :setting_show_subscribe_button_on_timeline, + :setting_show_followed_by, + :setting_follow_button_to_list_adder, + :setting_show_navigation_panel, + :setting_show_quote_button, + :setting_show_bookmark_button, + :setting_place_tab_bar_at_bottom, + :setting_show_tab_bar_label, + :setting_show_target, + :setting_enable_limited_timeline, notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 8badd9eaf9b485..ab61be4cdedbc1 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -7,6 +7,7 @@ import { useEmoji } from './emojis'; import resizeImage from '../utils/resize_image'; import { importFetchedAccounts } from './importer'; import { updateTimeline } from './timelines'; +import { getHomeVisibilities, getLimitedVisibilities } from 'mastodon/selectors'; import { showAlertForError } from './alerts'; import { showAlert } from './alerts'; import { openModal } from './modal'; @@ -140,6 +141,8 @@ export function submitCompose(routerHistory) { return function (dispatch, getState) { const status = getState().getIn(['compose', 'text'], ''); const media = getState().getIn(['compose', 'media_attachments']); + const homeVisibilities = getHomeVisibilities(getState()); + const limitedVisibilities = getLimitedVisibilities(getState()); if ((!status || !status.length) && media.size === 0) { return; @@ -178,10 +181,14 @@ export function submitCompose(routerHistory) { } }; - if (response.data.visibility !== 'direct') { + if (homeVisibilities.length == 0 || homeVisibilities.includes(response.data.visibility)) { insertIfOnline('home'); } + if (limitedVisibilities.includes(response.data.visibility)) { + insertIfOnline('limited'); + } + if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { insertIfOnline('community'); insertIfOnline('public'); diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index beb5c6a4a9de6c..bd535c91339133 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -8,6 +8,7 @@ import { connectTimeline, disconnectTimeline, } from './timelines'; +import { getHomeVisibilities } from 'mastodon/selectors'; import { updateNotifications, expandNotifications } from './notifications'; import { updateConversations } from './conversations'; import { @@ -44,10 +45,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti let pollingId; /** - * @param {function(Function, Function): void} fallback + * @param {function(Function, Function, Function): void} fallback */ const useFallback = fallback => { - fallback(dispatch, () => { + fallback(dispatch, getState, () => { pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000)); }); }; @@ -105,8 +106,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti * @param {Function} dispatch * @param {function(): void} done */ -const refreshHomeTimelineAndNotification = (dispatch, done) => { - dispatch(expandHomeTimeline({}, () => +const refreshHomeTimelineAndNotification = (dispatch, getState, done) => { + const visibilities = getHomeVisibilities(getState()); + + dispatch(expandHomeTimeline({ visibilities }, () => dispatch(expandNotifications({}, () => dispatch(fetchAnnouncements(done)))))); }; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 31ae09e4ac5013..9a61f71bee99a5 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,37 +1,42 @@ -import { importFetchedStatus, importFetchedStatuses } from './importer'; -import { submitMarkers } from './markers'; -import api, { getLinks } from 'mastodon/api'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import compareId from 'mastodon/compare_id'; -import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; - -export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; -export const TIMELINE_DELETE = 'TIMELINE_DELETE'; -export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; - -export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; -export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; -export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; - -export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; -export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; -export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; -export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; - -export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL'; - -export const loadPending = timeline => ({ +import { importFetchedStatus, importFetchedStatuses } from "./importer"; +import { submitMarkers } from "./markers"; +import api, { getLinks } from "mastodon/api"; +import { Map as ImmutableMap, List as ImmutableList } from "immutable"; +import compareId from "mastodon/compare_id"; +import { usePendingItems as preferPendingItems } from "mastodon/initial_state"; +import { + getHomeVisibilities, + getLimitedVisibilities, +} from "mastodon/selectors"; +import { uniq } from "../utils/uniq"; + +export const TIMELINE_UPDATE = "TIMELINE_UPDATE"; +export const TIMELINE_DELETE = "TIMELINE_DELETE"; +export const TIMELINE_CLEAR = "TIMELINE_CLEAR"; + +export const TIMELINE_EXPAND_REQUEST = "TIMELINE_EXPAND_REQUEST"; +export const TIMELINE_EXPAND_SUCCESS = "TIMELINE_EXPAND_SUCCESS"; +export const TIMELINE_EXPAND_FAIL = "TIMELINE_EXPAND_FAIL"; + +export const TIMELINE_SCROLL_TOP = "TIMELINE_SCROLL_TOP"; +export const TIMELINE_LOAD_PENDING = "TIMELINE_LOAD_PENDING"; +export const TIMELINE_DISCONNECT = "TIMELINE_DISCONNECT"; +export const TIMELINE_CONNECT = "TIMELINE_CONNECT"; + +export const TIMELINE_MARK_AS_PARTIAL = "TIMELINE_MARK_AS_PARTIAL"; + +export const loadPending = (timeline) => ({ type: TIMELINE_LOAD_PENDING, timeline, }); export function updateTimeline(timeline, status, accept) { return (dispatch, getState) => { - if (typeof accept === 'function' && !accept(status)) { + if (typeof accept === "function" && !accept(status)) { return; } - if (getState().getIn(['timelines', timeline, 'isPartial'])) { + if (getState().getIn(["timelines", timeline, "isPartial"])) { // Prevent new items from being added to a partial timeline, // since it will be reloaded anyway @@ -40,24 +45,45 @@ export function updateTimeline(timeline, status, accept) { dispatch(importFetchedStatus(status)); - dispatch({ - type: TIMELINE_UPDATE, - timeline, - status, - usePendingItems: preferPendingItems, - }); + const insertTimeline = (timeline) => { + dispatch({ + type: TIMELINE_UPDATE, + timeline, + status, + usePendingItems: preferPendingItems, + }); + }; + + const visibility = status.visibility_ex || status.visibility; + const homeVisibilities = getHomeVisibilities(getState()); + const limitedVisibilities = getLimitedVisibilities(getState()); + + if (timeline === "home") { + if ( + homeVisibilities.length == 0 || + homeVisibilities.includes(visibility) + ) { + insertTimeline("home"); + dispatch(submitMarkers()); + } - if (timeline === 'home') { - dispatch(submitMarkers()); + if (limitedVisibilities.includes(visibility)) { + insertTimeline("limited"); + } + } else { + insertTimeline(timeline); } }; -}; +} export function deleteFromTimelines(id) { return (dispatch, getState) => { - const accountId = getState().getIn(['statuses', id, 'account']); - const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')); - const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); + const accountId = getState().getIn(["statuses", id, "account"]); + const references = getState() + .get("statuses") + .filter((status) => status.get("reblog") === id) + .map((status) => status.get("id")); + const reblogOf = getState().getIn(["statuses", id, "reblog"], null); dispatch({ type: TIMELINE_DELETE, @@ -67,13 +93,13 @@ export function deleteFromTimelines(id) { reblogOf, }); }; -}; +} export function clearTimeline(timeline) { return (dispatch) => { dispatch({ type: TIMELINE_CLEAR, timeline }); }; -}; +} const noOp = () => {}; @@ -85,17 +111,26 @@ const parseTags = (tags = {}, mode) => { export function expandTimeline(timelineId, path, params = {}, done = noOp) { return (dispatch, getState) => { - const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); + const timeline = getState().getIn( + ["timelines", timelineId], + ImmutableMap() + ); const isLoadingMore = !!params.max_id; - if (timeline.get('isLoading')) { + if (timeline.get("isLoading")) { done(); return; } - if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) { - const a = timeline.getIn(['pendingItems', 0]); - const b = timeline.getIn(['items', 0]); + if ( + !params.max_id && + !params.pinned && + timeline.get("items", ImmutableList()).size + + timeline.get("pendingItems", ImmutableList()).size > + 0 + ) { + const a = timeline.getIn(["pendingItems", 0]); + const b = timeline.getIn(["items", 0]); if (a && b && compareId(a, b) > 0) { params.since_id = a; @@ -108,37 +143,117 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { dispatch(expandTimelineRequest(timelineId, isLoadingMore)); - api(getState).get(path, { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); - - if (timelineId === 'home') { - dispatch(submitMarkers()); - } - }).catch(error => { - dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); - }).finally(() => { - done(); - }); + api(getState) + .get(path, { params }) + .then((response) => { + const next = getLinks(response).refs.find( + (link) => link.rel === "next" + ); + dispatch(importFetchedStatuses(response.data)); + dispatch( + expandTimelineSuccess( + timelineId, + response.data, + next ? next.uri : null, + response.status === 206, + isLoadingRecent, + isLoadingMore, + isLoadingRecent && preferPendingItems + ) + ); + + if (timelineId === "home") { + dispatch(submitMarkers()); + } + }) + .catch((error) => { + dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); + }) + .finally(() => { + 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 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 } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); -export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); -export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); -export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); -export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { - return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, { - max_id: maxId, - any: parseTags(tags, 'any'), - all: parseTags(tags, 'all'), - none: parseTags(tags, 'none'), - local: local, - }, done); +} + +export const expandHomeTimeline = ({ maxId, visibilities } = {}, done = noOp) => + expandTimeline( + "home", + "/api/v1/timelines/home", + { max_id: maxId, visibilities: visibilities }, + done + ); +export const expandLimitedTimeline = ( + { maxId, visibilities } = {}, + done = noOp +) => + expandTimeline( + "limited", + "/api/v1/timelines/home", + { max_id: maxId, visibilities: visibilities }, + done + ); +export const expandPublicTimeline = ( + { maxId, onlyMedia, onlyRemote } = {}, + done = noOp +) => + expandTimeline( + `public${onlyRemote ? ":remote" : ""}${onlyMedia ? ":media" : ""}`, + "/api/v1/timelines/public", + { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, + done + ); +export const expandCommunityTimeline = ( + { maxId, onlyMedia } = {}, + done = noOp +) => + expandTimeline( + `community${onlyMedia ? ":media" : ""}`, + "/api/v1/timelines/public", + { local: true, max_id: maxId, only_media: !!onlyMedia }, + done + ); +export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => + expandTimeline( + `account:${accountId}${withReplies ? ":with_replies" : ""}`, + `/api/v1/accounts/${accountId}/statuses`, + { exclude_replies: !withReplies, max_id: maxId } + ); +export const expandAccountFeaturedTimeline = (accountId) => + expandTimeline( + `account:${accountId}:pinned`, + `/api/v1/accounts/${accountId}/statuses`, + { pinned: true } + ); +export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => + expandTimeline( + `account:${accountId}:media`, + `/api/v1/accounts/${accountId}/statuses`, + { max_id: maxId, only_media: true, limit: 40 } + ); +export const expandListTimeline = (id, { maxId } = {}, done = noOp) => + expandTimeline( + `list:${id}`, + `/api/v1/timelines/list/${id}`, + { max_id: maxId }, + done + ); +export const expandHashtagTimeline = ( + hashtag, + { maxId, tags, local } = {}, + done = noOp +) => { + return expandTimeline( + `hashtag:${hashtag}${local ? ":local" : ""}`, + `/api/v1/timelines/tag/${hashtag}`, + { + max_id: maxId, + any: parseTags(tags, "any"), + all: parseTags(tags, "all"), + none: parseTags(tags, "none"), + local: local, + }, + done + ); }; export function expandTimelineRequest(timeline, isLoadingMore) { @@ -147,9 +262,17 @@ export function expandTimelineRequest(timeline, isLoadingMore) { timeline, skipLoading: !isLoadingMore, }; -}; +} -export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) { +export function expandTimelineSuccess( + timeline, + statuses, + next, + partial, + isLoadingRecent, + isLoadingMore, + usePendingItems +) { return { type: TIMELINE_EXPAND_SUCCESS, timeline, @@ -160,7 +283,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi usePendingItems, skipLoading: !isLoadingMore, }; -}; +} export function expandTimelineFail(timeline, error, isLoadingMore) { return { @@ -168,9 +291,9 @@ export function expandTimelineFail(timeline, error, isLoadingMore) { timeline, error, skipLoading: !isLoadingMore, - skipNotFound: timeline.startsWith('account:'), + skipNotFound: timeline.startsWith("account:"), }; -}; +} export function scrollTopTimeline(timeline, top) { return { @@ -178,22 +301,22 @@ export function scrollTopTimeline(timeline, top) { timeline, top, }; -}; +} export function connectTimeline(timeline) { return { type: TIMELINE_CONNECT, timeline, }; -}; +} -export const disconnectTimeline = timeline => ({ +export const disconnectTimeline = (timeline) => ({ type: TIMELINE_DISCONNECT, timeline, usePendingItems: preferPendingItems, }); -export const markAsPartial = timeline => ({ +export const markAsPartial = (timeline) => ({ type: TIMELINE_MARK_AS_PARTIAL, timeline, }); diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 663dd324f35631..61e17fa787698b 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -1,45 +1,103 @@ -import React from 'react'; -import ComposeFormContainer from './containers/compose_form_container'; -import NavigationContainer from './containers/navigation_container'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; -import { mountCompose, unmountCompose } from '../../actions/compose'; -import { Link } from 'react-router-dom'; -import { injectIntl, defineMessages } from 'react-intl'; -import SearchContainer from './containers/search_container'; -import Motion from '../ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import SearchResultsContainer from './containers/search_results_container'; -import { changeComposing } from '../../actions/compose'; -import { openModal } from 'mastodon/actions/modal'; -import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; -import { mascot } from '../../initial_state'; -import Icon from 'mastodon/components/icon'; -import { logOut } from 'mastodon/utils/log_out'; +import React from "react"; +import ComposeFormContainer from "./containers/compose_form_container"; +import NavigationContainer from "./containers/navigation_container"; +import PropTypes from "prop-types"; +import ImmutablePropTypes from "react-immutable-proptypes"; +import { connect } from "react-redux"; +import { mountCompose, unmountCompose } from "../../actions/compose"; +import { Link } from "react-router-dom"; +import { injectIntl, defineMessages } from "react-intl"; +import SearchContainer from "./containers/search_container"; +import Motion from "../ui/util/optional_motion"; +import spring from "react-motion/lib/spring"; +import SearchResultsContainer from "./containers/search_results_container"; +import { changeComposing } from "../../actions/compose"; +import { openModal } from "mastodon/actions/modal"; +import elephantUIPlane from "../../../images/elephant_ui_plane.svg"; +import { + mascot, + show_tab_bar_label, + enable_limited_timeline, +} from "../../initial_state"; +import Icon from "mastodon/components/icon"; +import { logOut } from "mastodon/utils/log_out"; const messages = defineMessages({ - start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, - home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, - notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, - public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, - community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, - preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, - logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, - compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' }, - logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, - logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' }, + short_start: { + id: "navigation_bar.short.getting_started", + defaultMessage: "Started", + }, + short_home_timeline: { + id: "navigation_bar.short.home", + defaultMessage: "Home", + }, + short_limited_timeline: { + id: "navigation_bar.short.limited_timeline", + defaultMessage: "Ltd.", + }, + short_notifications: { + id: "navigation_bar.short.notifications", + defaultMessage: "Notif.", + }, + short_public: { + id: "navigation_bar.short.public_timeline", + defaultMessage: "FTL", + }, + short_community: { + id: "navigation_bar.short.community_timeline", + defaultMessage: "LTL", + }, + short_lists: { id: "navigation_bar.short.lists", defaultMessage: "Lists" }, + short_preferences: { + id: "navigation_bar.short.preferences", + defaultMessage: "Pref.", + }, + short_logout: { id: "navigation_bar.short.logout", defaultMessage: "Logout" }, + start: { id: "getting_started.heading", defaultMessage: "Getting started" }, + home_timeline: { id: "tabs_bar.home", defaultMessage: "Home" }, + limited_timeline: { + id: "tabs_bar.limited_timeline", + defaultMessage: "Limited home", + }, + notifications: { + id: "tabs_bar.notifications", + defaultMessage: "Notifications", + }, + public: { + id: "navigation_bar.public_timeline", + defaultMessage: "Federated timeline", + }, + community: { + id: "navigation_bar.community_timeline", + defaultMessage: "Local timeline", + }, + preferences: { + id: "navigation_bar.preferences", + defaultMessage: "Preferences", + }, + logout: { id: "navigation_bar.logout", defaultMessage: "Logout" }, + compose: { id: "navigation_bar.compose", defaultMessage: "Compose new toot" }, + logoutMessage: { + id: "confirmations.logout.message", + defaultMessage: "Are you sure you want to log out?", + }, + logoutConfirm: { + id: "confirmations.logout.confirm", + defaultMessage: "Log out", + }, }); const mapStateToProps = (state, ownProps) => ({ - columns: state.getIn(['settings', 'columns']), - showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : ownProps.isSearchPage, + columns: state.getIn(["settings", "columns"]), + showSearch: ownProps.multiColumn + ? state.getIn(["search", "submitted"]) && !state.getIn(["search", "hidden"]) + : ownProps.isSearchPage, }); -export default @connect(mapStateToProps) +export default +@connect(mapStateToProps) @injectIntl class Compose extends React.PureComponent { - static propTypes = { dispatch: PropTypes.func.isRequired, columns: ImmutablePropTypes.list.isRequired, @@ -49,7 +107,7 @@ class Compose extends React.PureComponent { intl: PropTypes.object.isRequired, }; - componentDidMount () { + componentDidMount() { const { isSearchPage } = this.props; if (!isSearchPage) { @@ -57,7 +115,7 @@ class Compose extends React.PureComponent { } } - componentWillUnmount () { + componentWillUnmount() { const { isSearchPage } = this.props; if (!isSearchPage) { @@ -65,78 +123,274 @@ class Compose extends React.PureComponent { } } - handleLogoutClick = e => { + handleLogoutClick = (e) => { const { dispatch, intl } = this.props; e.preventDefault(); e.stopPropagation(); - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.logoutMessage), - confirm: intl.formatMessage(messages.logoutConfirm), - closeWhenConfirm: false, - onConfirm: () => logOut(), - })); + dispatch( + openModal("CONFIRM", { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + closeWhenConfirm: false, + onConfirm: () => logOut(), + }) + ); return false; - } + }; onFocus = () => { this.props.dispatch(changeComposing(true)); - } + }; onBlur = () => { this.props.dispatch(changeComposing(false)); + }; + + tab(id) { + const { + columns, + intl: { formatMessage }, + } = this.props; + + if (!columns.some((column) => column.get("id") === id)) { + const tabParams = { + START: { + to: "/getting-started", + title: formatMessage(messages.start), + label: formatMessage(messages.short_start), + icon_id: "bars", + }, + HOME: { + to: "/timelines/home", + title: formatMessage(messages.home_timeline), + label: formatMessage(messages.short_home_timeline), + icon_id: "home", + }, + LIMITED: { + to: "/timelines/limited", + title: formatMessage(messages.limited_timeline), + label: formatMessage(messages.short_limited_timeline), + icon_id: "lock", + }, + NOTIFICATIONS: { + to: "/notifications", + title: formatMessage(messages.notifications), + label: formatMessage(messages.short_notifications), + icon_id: "bell", + }, + COMMUNITY: { + to: "/timelines/public/local", + title: formatMessage(messages.community), + label: formatMessage(messages.short_community), + icon_id: "users", + }, + PUBLIC: { + to: "/timelines/public", + title: formatMessage(messages.public), + label: formatMessage(messages.short_public), + icon_id: "globe", + }, + PREFERENCES: { + href: "/settings/preferences", + title: formatMessage(messages.preferences), + label: formatMessage(messages.short_preferences), + icon_id: "cog", + }, + SIGN_OUT: { + href: "/auth/sign_out", + title: formatMessage(messages.logout), + label: formatMessage(messages.short_logout), + icon_id: "sign-out", + method: "delete", + }, + }; + + const { href, to, title, label, icon_id, method } = tabParams[id]; + + const icon = + id === "NOTIFICATIONS" ? ( + + ) : ( + + ); + + if (href) { + return ( + + {icon} + {label} + + ); + } else { + return ( + + {icon} + {label} + + ); + } + } + return null; } - render () { + render() { const { multiColumn, showSearch, isSearchPage, intl } = this.props; - let header = ''; + let header = ""; if (multiColumn) { - const { columns } = this.props; + const defaultTabIds = enable_limited_timeline + ? [ + "START", + "HOME", + "LIMITED", + "NOTIFICATIONS", + "COMMUNITY", + "PUBLIC", + "PREFERENCES", + "SIGN_OUT", + ] + : [ + "START", + "HOME", + "NOTIFICATIONS", + "COMMUNITY", + "PUBLIC", + "PREFERENCES", + "SIGN_OUT", + ]; + + let tabs = defaultTabIds; + header = ( -