mirror of
https://github.com/Siphonay/mastodon
synced 2025-02-12 12:24:46 +00:00
Merge pull request #2256 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
b052a7ed95
@ -318,7 +318,6 @@ RSpec/LetSetup:
|
||||
- 'spec/controllers/api/v1/admin/accounts_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/filters_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/followed_tags_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/tags_controller_spec.rb'
|
||||
- 'spec/controllers/api/v2/admin/accounts_controller_spec.rb'
|
||||
- 'spec/controllers/api/v2/filters/keywords_controller_spec.rb'
|
||||
- 'spec/controllers/api/v2/filters/statuses_controller_spec.rb'
|
||||
@ -440,45 +439,6 @@ RSpec/SubjectStub:
|
||||
- 'spec/services/unallow_domain_service_spec.rb'
|
||||
- 'spec/validators/blacklisted_email_validator_spec.rb'
|
||||
|
||||
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
|
||||
RSpec/VerifiedDoubles:
|
||||
Exclude:
|
||||
- 'spec/controllers/admin/change_emails_controller_spec.rb'
|
||||
- 'spec/controllers/admin/confirmations_controller_spec.rb'
|
||||
- 'spec/controllers/admin/disputes/appeals_controller_spec.rb'
|
||||
- 'spec/controllers/admin/domain_allows_controller_spec.rb'
|
||||
- 'spec/controllers/admin/domain_blocks_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/reports_controller_spec.rb'
|
||||
- 'spec/controllers/api/web/embeds_controller_spec.rb'
|
||||
- 'spec/controllers/auth/sessions_controller_spec.rb'
|
||||
- 'spec/controllers/disputes/appeals_controller_spec.rb'
|
||||
- 'spec/helpers/statuses_helper_spec.rb'
|
||||
- 'spec/lib/suspicious_sign_in_detector_spec.rb'
|
||||
- 'spec/models/account/field_spec.rb'
|
||||
- 'spec/models/session_activation_spec.rb'
|
||||
- 'spec/models/setting_spec.rb'
|
||||
- 'spec/services/account_search_service_spec.rb'
|
||||
- 'spec/services/post_status_service_spec.rb'
|
||||
- 'spec/services/search_service_spec.rb'
|
||||
- 'spec/validators/blacklisted_email_validator_spec.rb'
|
||||
- 'spec/validators/disallowed_hashtags_validator_spec.rb'
|
||||
- 'spec/validators/email_mx_validator_spec.rb'
|
||||
- 'spec/validators/follow_limit_validator_spec.rb'
|
||||
- 'spec/validators/note_length_validator_spec.rb'
|
||||
- 'spec/validators/poll_validator_spec.rb'
|
||||
- 'spec/validators/status_length_validator_spec.rb'
|
||||
- 'spec/validators/status_pin_validator_spec.rb'
|
||||
- 'spec/validators/unique_username_validator_spec.rb'
|
||||
- 'spec/validators/unreserved_username_validator_spec.rb'
|
||||
- 'spec/validators/url_validator_spec.rb'
|
||||
- 'spec/views/statuses/show.html.haml_spec.rb'
|
||||
- 'spec/workers/activitypub/processing_worker_spec.rb'
|
||||
- 'spec/workers/admin/domain_purge_worker_spec.rb'
|
||||
- 'spec/workers/domain_block_worker_spec.rb'
|
||||
- 'spec/workers/domain_clear_media_worker_spec.rb'
|
||||
- 'spec/workers/feed_insert_worker_spec.rb'
|
||||
- 'spec/workers/regeneration_worker_spec.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Rails/ApplicationController:
|
||||
Exclude:
|
||||
@ -759,7 +719,6 @@ Rails/WhereExists:
|
||||
- 'app/workers/move_worker.rb'
|
||||
- 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
|
||||
- 'lib/tasks/tests.rake'
|
||||
- 'spec/controllers/api/v1/tags_controller_spec.rb'
|
||||
- 'spec/models/account_spec.rb'
|
||||
- 'spec/services/activitypub/process_collection_service_spec.rb'
|
||||
- 'spec/services/purge_domain_service_spec.rb'
|
||||
|
@ -106,7 +106,7 @@ GEM
|
||||
aws-sdk-kms (1.67.0)
|
||||
aws-sdk-core (~> 3, >= 3.174.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.125.0)
|
||||
aws-sdk-s3 (1.126.0)
|
||||
aws-sdk-core (~> 3, >= 3.174.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
|
@ -28,6 +28,7 @@ module Admin
|
||||
authorize :webhook, :create?
|
||||
|
||||
@webhook = Webhook.new(resource_params)
|
||||
@webhook.current_account = current_account
|
||||
|
||||
if @webhook.save
|
||||
redirect_to admin_webhook_path(@webhook)
|
||||
@ -39,10 +40,12 @@ module Admin
|
||||
def update
|
||||
authorize @webhook, :update?
|
||||
|
||||
@webhook.current_account = current_account
|
||||
|
||||
if @webhook.update(resource_params)
|
||||
redirect_to admin_webhook_path(@webhook)
|
||||
else
|
||||
render :show
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -19,6 +19,11 @@ class Api::V1::ConversationsController < Api::BaseController
|
||||
render json: @conversation, serializer: REST::ConversationSerializer
|
||||
end
|
||||
|
||||
def unread
|
||||
@conversation.update!(unread: true)
|
||||
render json: @conversation, serializer: REST::ConversationSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
@conversation.destroy!
|
||||
render_empty
|
||||
|
@ -8,11 +8,15 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
|
||||
|
||||
def show
|
||||
cache_if_unauthenticated!
|
||||
render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer
|
||||
render json: status_edits, each_serializer: REST::StatusEditSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def status_edits
|
||||
@status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)]
|
||||
end
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
|
@ -18,6 +18,14 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
|
||||
|
||||
private
|
||||
|
||||
def next_path
|
||||
api_v2_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||
end
|
||||
|
||||
def prev_path
|
||||
api_v2_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
|
||||
end
|
||||
|
||||
def filtered_accounts
|
||||
AccountFilter.new(translated_filter_params).results
|
||||
end
|
||||
|
@ -24,13 +24,4 @@ module SettingsHelper
|
||||
safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
|
||||
end
|
||||
end
|
||||
|
||||
def picture_hint(hint, picture)
|
||||
if picture.original_filename.nil?
|
||||
hint
|
||||
else
|
||||
link = link_to t('generic.delete'), settings_profile_picture_path(picture.name.to_s), data: { method: :delete }
|
||||
safe_join([hint, link], '<br/>'.html_safe)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,44 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ShortNumber from 'flavours/glitch/components/short_number';
|
||||
|
||||
export default class AutosuggestHashtag extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
tag: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
url: PropTypes.string,
|
||||
history: PropTypes.array,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tag } = this.props;
|
||||
const weeklyUses = tag.history && (
|
||||
<ShortNumber
|
||||
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='autosuggest-hashtag'>
|
||||
<div className='autosuggest-hashtag__name'>
|
||||
#<strong>{tag.name}</strong>
|
||||
</div>
|
||||
{tag.history !== undefined && (
|
||||
<div className='autosuggest-hashtag__uses'>
|
||||
<FormattedMessage
|
||||
id='autosuggest_hashtag.per_week'
|
||||
defaultMessage='{count} per week'
|
||||
values={{ count: weeklyUses }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ShortNumber from 'flavours/glitch/components/short_number';
|
||||
|
||||
interface Props {
|
||||
tag: {
|
||||
name: string;
|
||||
url?: string;
|
||||
history?: Array<{
|
||||
uses: number;
|
||||
accounts: string;
|
||||
day: string;
|
||||
}>;
|
||||
following?: boolean;
|
||||
type: 'hashtag';
|
||||
};
|
||||
}
|
||||
|
||||
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => {
|
||||
const weeklyUses = tag.history && (
|
||||
<ShortNumber
|
||||
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='autosuggest-hashtag'>
|
||||
<div className='autosuggest-hashtag__name'>
|
||||
#<strong>{tag.name}</strong>
|
||||
</div>
|
||||
{tag.history !== undefined && (
|
||||
<div className='autosuggest-hashtag__uses'>
|
||||
<FormattedMessage
|
||||
id='autosuggest_hashtag.per_week'
|
||||
defaultMessage='{count} per week'
|
||||
values={{ count: weeklyUses }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -8,9 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
|
||||
|
||||
import AutosuggestEmoji from './autosuggest_emoji';
|
||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
||||
|
||||
|
||||
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
||||
|
||||
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
|
||||
let word;
|
||||
|
@ -10,7 +10,7 @@ import Textarea from 'react-textarea-autosize';
|
||||
import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
|
||||
|
||||
import AutosuggestEmoji from './autosuggest_emoji';
|
||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
||||
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
||||
|
||||
const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||
let word;
|
||||
|
@ -15,13 +15,14 @@ import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/col
|
||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||
import StatusList from 'flavours/glitch/components/status_list';
|
||||
import Column from 'flavours/glitch/features/ui/components/column';
|
||||
import { getStatusList } from 'flavours/glitch/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']),
|
||||
statusIds: getStatusList(state, 'bookmarks'),
|
||||
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
|
||||
});
|
||||
|
@ -142,11 +142,8 @@ class CommunityTimeline extends PureComponent {
|
||||
<ColumnSettingsContainer columnId={columnId} />
|
||||
</ColumnHeader>
|
||||
|
||||
<DismissableBanner id='community_timeline'>
|
||||
<FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} />
|
||||
</DismissableBanner>
|
||||
|
||||
<StatusListContainer
|
||||
prepend={<DismissableBanner id='community_timeline'><FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} /></DismissableBanner>}
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`community_timeline-${columnId}`}
|
||||
timelineId={`community${onlyMedia ? ':media' : ''}`}
|
||||
|
@ -391,7 +391,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||
{button || <img
|
||||
className={classNames('emojione', { 'pulse-loading': active && loading })}
|
||||
alt='🙂'
|
||||
src={`${assetHost}/emoji/1f602.svg`}
|
||||
src={`${assetHost}/emoji/1f642.svg`}
|
||||
/>}
|
||||
</div>
|
||||
|
||||
|
@ -35,7 +35,7 @@ class Links extends PureComponent {
|
||||
|
||||
const banner = (
|
||||
<DismissableBanner id='explore/links'>
|
||||
<FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These news stories are being talked about by people on this and other servers of the decentralized network right now.' />
|
||||
<FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.' />
|
||||
</DismissableBanner>
|
||||
);
|
||||
|
||||
|
@ -11,9 +11,10 @@ import { debounce } from 'lodash';
|
||||
import { fetchTrendingStatuses, expandTrendingStatuses } from 'flavours/glitch/actions/trends';
|
||||
import DismissableBanner from 'flavours/glitch/components/dismissable_banner';
|
||||
import StatusList from 'flavours/glitch/components/status_list';
|
||||
import { getStatusList } from 'flavours/glitch/selectors';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
statusIds: state.getIn(['status_lists', 'trending', 'items']),
|
||||
statusIds: getStatusList(state, 'trending'),
|
||||
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'trending', 'next']),
|
||||
});
|
||||
@ -46,7 +47,7 @@ class Statuses extends PureComponent {
|
||||
return (
|
||||
<>
|
||||
<DismissableBanner id='explore/statuses'>
|
||||
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These posts from this and other servers in the decentralized network are gaining traction on this server right now.' />
|
||||
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.' />
|
||||
</DismissableBanner>
|
||||
|
||||
<StatusList
|
||||
|
@ -36,7 +36,7 @@ class Tags extends PureComponent {
|
||||
|
||||
const banner = (
|
||||
<DismissableBanner id='explore/tags'>
|
||||
<FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These hashtags are gaining traction among people on this and other servers of the decentralized network right now.' />
|
||||
<FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.' />
|
||||
</DismissableBanner>
|
||||
);
|
||||
|
||||
|
@ -15,13 +15,14 @@ import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'flavours/glit
|
||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||
import StatusList from 'flavours/glitch/components/status_list';
|
||||
import Column from 'flavours/glitch/features/ui/components/column';
|
||||
import { getStatusList } from 'flavours/glitch/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
||||
statusIds: getStatusList(state, 'favourites'),
|
||||
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
||||
});
|
||||
|
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import DismissableBanner from 'flavours/glitch/components/dismissable_banner';
|
||||
import background from 'mastodon/../images/friends-cropped.png';
|
||||
|
||||
|
||||
export const ExplorePrompt = () => (
|
||||
<DismissableBanner id='home.explore_prompt'>
|
||||
<img src={background} alt='' className='dismissable-banner__background-image' />
|
||||
|
||||
<h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
|
||||
<p><FormattedMessage id='home.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. It's looking pretty quiet right now, so how about:" /></p>
|
||||
|
||||
<div className='dismissable-banner__message__actions'>
|
||||
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
|
||||
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
|
||||
</div>
|
||||
</DismissableBanner>
|
||||
);
|
@ -5,35 +5,59 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements';
|
||||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
||||
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
|
||||
import Column from 'flavours/glitch/components/column';
|
||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
|
||||
import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator';
|
||||
import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container';
|
||||
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
|
||||
import { me } from 'flavours/glitch/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 StatusListContainer from '../ui/containers/status_list_container';
|
||||
|
||||
import { ExplorePrompt } from './components/explore_prompt';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.home', defaultMessage: 'Home' },
|
||||
show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' },
|
||||
hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' },
|
||||
});
|
||||
|
||||
const getHomeFeedSpeed = createSelector([
|
||||
state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
|
||||
state => state.get('statuses'),
|
||||
], (statusIds, statusMap) => {
|
||||
const statuses = statusIds.map(id => statusMap.get(id)).filter(status => status.get('account') !== me).take(20);
|
||||
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
|
||||
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
|
||||
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
||||
|
||||
return {
|
||||
gap: averageGap,
|
||||
newest,
|
||||
};
|
||||
});
|
||||
|
||||
const homeTooSlow = createSelector(getHomeFeedSpeed, speed =>
|
||||
speed.gap > (30 * 60) // If the average gap between posts is more than 20 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']),
|
||||
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
|
||||
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
|
||||
showAnnouncements: state.getIn(['announcements', 'show']),
|
||||
tooSlow: homeTooSlow(state),
|
||||
regex: state.getIn(['settings', 'home', 'regex', 'body']),
|
||||
});
|
||||
|
||||
@ -53,6 +77,7 @@ class HomeTimeline extends PureComponent {
|
||||
hasAnnouncements: PropTypes.bool,
|
||||
unreadAnnouncements: PropTypes.number,
|
||||
showAnnouncements: PropTypes.bool,
|
||||
tooSlow: PropTypes.bool,
|
||||
regex: PropTypes.string,
|
||||
};
|
||||
|
||||
@ -123,11 +148,11 @@ class HomeTimeline extends PureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
||||
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
||||
const pinned = !!columnId;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
let announcementsButton = null;
|
||||
let announcementsButton, banner;
|
||||
|
||||
if (hasAnnouncements) {
|
||||
announcementsButton = (
|
||||
@ -142,6 +167,10 @@ class HomeTimeline extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (tooSlow) {
|
||||
banner = <ExplorePrompt />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} name='home' label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
@ -161,11 +190,13 @@ class HomeTimeline extends PureComponent {
|
||||
|
||||
{signedIn ? (
|
||||
<StatusListContainer
|
||||
prepend={banner}
|
||||
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. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />}
|
||||
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up.' />}
|
||||
bindToDocument={!multiColumn}
|
||||
regex={this.props.regex}
|
||||
/>
|
||||
|
@ -8,17 +8,19 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchPinnedStatuses } from 'flavours/glitch/actions/pin_statuses';
|
||||
import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
|
||||
import StatusList from 'flavours/glitch/components/status_list';
|
||||
import Column from 'flavours/glitch/features/ui/components/column';
|
||||
import { getStatusList } from 'flavours/glitch/selectors';
|
||||
|
||||
import { fetchPinnedStatuses } from '../../actions/pin_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: state.getIn(['status_lists', 'pins', 'items']),
|
||||
statusIds: getStatusList(state, 'pins'),
|
||||
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
|
||||
});
|
||||
|
||||
|
@ -146,11 +146,8 @@ class PublicTimeline extends PureComponent {
|
||||
<ColumnSettingsContainer columnId={columnId} />
|
||||
</ColumnHeader>
|
||||
|
||||
<DismissableBanner id='public_timeline'>
|
||||
<FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.' />
|
||||
</DismissableBanner>
|
||||
|
||||
<StatusListContainer
|
||||
prepend={<DismissableBanner id='public_timeline'><FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.' /></DismissableBanner>}
|
||||
timelineId={`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
trackScroll={!pinned}
|
||||
|
@ -8,6 +8,7 @@ import { Link, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { fetchServer } from 'flavours/glitch/actions/server';
|
||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
@ -29,6 +30,9 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
openClosedRegistrationsModal() {
|
||||
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
|
||||
},
|
||||
dispatchServer() {
|
||||
dispatch(fetchServer());
|
||||
}
|
||||
});
|
||||
|
||||
class Header extends PureComponent {
|
||||
@ -41,8 +45,14 @@ class Header extends PureComponent {
|
||||
openClosedRegistrationsModal: PropTypes.func,
|
||||
location: PropTypes.object,
|
||||
signupUrl: PropTypes.string.isRequired,
|
||||
dispatchServer: PropTypes.func
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatchServer } = this.props;
|
||||
dispatchServer();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { signedIn } = this.context.identity;
|
||||
const { location, openClosedRegistrationsModal, signupUrl } = this.props;
|
||||
|
@ -137,3 +137,7 @@ export const getAccountHidden = createSelector([
|
||||
], (hidden, followingOrRequested, isSelf) => {
|
||||
return hidden && !(isSelf || followingOrRequested);
|
||||
});
|
||||
|
||||
export const getStatusList = createSelector([
|
||||
(state, type) => state.getIn(['status_lists', type, 'items']),
|
||||
], (items) => items.toList());
|
||||
|
@ -960,26 +960,70 @@ $ui-header-height: 55px;
|
||||
}
|
||||
|
||||
.dismissable-banner {
|
||||
background: $ui-base-color;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
position: relative;
|
||||
margin: 10px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid $highlight-text-color;
|
||||
background: rgba($highlight-text-color, 0.15);
|
||||
padding-inline-end: 45px;
|
||||
overflow: hidden;
|
||||
|
||||
&__background-image {
|
||||
width: 125%;
|
||||
position: absolute;
|
||||
bottom: -25%;
|
||||
inset-inline-end: -25%;
|
||||
z-index: -1;
|
||||
opacity: 0.15;
|
||||
mix-blend-mode: luminosity;
|
||||
}
|
||||
|
||||
&__message {
|
||||
flex: 1 1 auto;
|
||||
padding: 20px 15px;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
padding: 15px;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 500;
|
||||
color: $primary-text-color;
|
||||
|
||||
p {
|
||||
margin-bottom: 15px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: $highlight-text-color;
|
||||
font-size: 22px;
|
||||
line-height: 33px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.button-tertiary {
|
||||
background: rgba($ui-base-color, 0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
}
|
||||
|
||||
&__action {
|
||||
padding: 15px;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
inset-inline-end: 0;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
|
||||
.icon-button {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -653,11 +653,6 @@ html {
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
.dismissable-banner {
|
||||
border-left: 1px solid lighten($ui-base-color, 8%);
|
||||
border-right: 1px solid lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
.status__content,
|
||||
.reply-indicator__content {
|
||||
a {
|
||||
|
BIN
app/javascript/images/friends-cropped.png
Executable file
BIN
app/javascript/images/friends-cropped.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 189 KiB |
@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
@ -49,6 +49,7 @@ class Account extends ImmutablePureComponent {
|
||||
actionTitle: PropTypes.string,
|
||||
defaultAction: PropTypes.string,
|
||||
onActionClick: PropTypes.func,
|
||||
withBio: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -80,7 +81,7 @@ class Account extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props;
|
||||
const { account, intl, hidden, withBio, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return <EmptyAccount size={size} minimal={minimal} />;
|
||||
@ -171,6 +172,15 @@ class Account extends ImmutablePureComponent {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{withBio && (account.get('note').length > 0 ? (
|
||||
<div
|
||||
className='account__note translate'
|
||||
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||
/>
|
||||
) : (
|
||||
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,44 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ShortNumber from 'mastodon/components/short_number';
|
||||
|
||||
export default class AutosuggestHashtag extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
tag: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
url: PropTypes.string,
|
||||
history: PropTypes.array,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tag } = this.props;
|
||||
const weeklyUses = tag.history && (
|
||||
<ShortNumber
|
||||
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='autosuggest-hashtag'>
|
||||
<div className='autosuggest-hashtag__name'>
|
||||
#<strong>{tag.name}</strong>
|
||||
</div>
|
||||
{tag.history !== undefined && (
|
||||
<div className='autosuggest-hashtag__uses'>
|
||||
<FormattedMessage
|
||||
id='autosuggest_hashtag.per_week'
|
||||
defaultMessage='{count} per week'
|
||||
values={{ count: weeklyUses }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
42
app/javascript/mastodon/components/autosuggest_hashtag.tsx
Normal file
42
app/javascript/mastodon/components/autosuggest_hashtag.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ShortNumber from 'mastodon/components/short_number';
|
||||
|
||||
interface Props {
|
||||
tag: {
|
||||
name: string;
|
||||
url?: string;
|
||||
history?: Array<{
|
||||
uses: number;
|
||||
accounts: string;
|
||||
day: string;
|
||||
}>;
|
||||
following?: boolean;
|
||||
type: 'hashtag';
|
||||
};
|
||||
}
|
||||
|
||||
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => {
|
||||
const weeklyUses = tag.history && (
|
||||
<ShortNumber
|
||||
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='autosuggest-hashtag'>
|
||||
<div className='autosuggest-hashtag__name'>
|
||||
#<strong>{tag.name}</strong>
|
||||
</div>
|
||||
{tag.history !== undefined && (
|
||||
<div className='autosuggest-hashtag__uses'>
|
||||
<FormattedMessage
|
||||
id='autosuggest_hashtag.per_week'
|
||||
defaultMessage='{count} per week'
|
||||
values={{ count: weeklyUses }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -8,7 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||
|
||||
import AutosuggestEmoji from './autosuggest_emoji';
|
||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
||||
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
||||
|
||||
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
|
||||
let word;
|
||||
|
@ -10,7 +10,7 @@ import Textarea from 'react-textarea-autosize';
|
||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||
|
||||
import AutosuggestEmoji from './autosuggest_emoji';
|
||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
||||
import { AutosuggestHashtag } from './autosuggest_hashtag';
|
||||
|
||||
const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||
let word;
|
||||
|
@ -1,11 +1,27 @@
|
||||
import { Icon } from './icon';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
const stripRelMe = (html: string) => {
|
||||
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
||||
|
||||
document.querySelectorAll<HTMLAnchorElement>('a[rel]').forEach((link) => {
|
||||
link.rel = link.rel
|
||||
.split(' ')
|
||||
.filter((x: string) => x !== 'me')
|
||||
.join(' ');
|
||||
});
|
||||
|
||||
const body = document.querySelector('body');
|
||||
return body ? { __html: body.innerHTML } : undefined;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
link: string;
|
||||
}
|
||||
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
||||
<span className='verified-badge'>
|
||||
<Icon id='check' className='verified-badge__mark' />
|
||||
<span dangerouslySetInnerHTML={{ __html: link }} />
|
||||
<span dangerouslySetInnerHTML={stripRelMe(link)} />
|
||||
</span>
|
||||
);
|
||||
|
@ -15,13 +15,14 @@ import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import StatusList from 'mastodon/components/status_list';
|
||||
import Column from 'mastodon/features/ui/components/column';
|
||||
import { getStatusList } from 'mastodon/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']),
|
||||
statusIds: getStatusList(state, 'bookmarks'),
|
||||
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
|
||||
});
|
||||
|
@ -140,11 +140,8 @@ class CommunityTimeline extends PureComponent {
|
||||
<ColumnSettingsContainer columnId={columnId} />
|
||||
</ColumnHeader>
|
||||
|
||||
<DismissableBanner id='community_timeline'>
|
||||
<FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} />
|
||||
</DismissableBanner>
|
||||
|
||||
<StatusListContainer
|
||||
prepend={<DismissableBanner id='community_timeline'><FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} /></DismissableBanner>}
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`community_timeline-${columnId}`}
|
||||
timelineId={`community${onlyMedia ? ':media' : ''}`}
|
||||
|
@ -389,7 +389,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||
{button || <img
|
||||
className={classNames('emojione', { 'pulse-loading': active && loading })}
|
||||
alt='🙂'
|
||||
src={`${assetHost}/emoji/1f602.svg`}
|
||||
src={`${assetHost}/emoji/1f642.svg`}
|
||||
/>}
|
||||
</div>
|
||||
|
||||
|
@ -35,7 +35,7 @@ class Links extends PureComponent {
|
||||
|
||||
const banner = (
|
||||
<DismissableBanner id='explore/links'>
|
||||
<FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These news stories are being talked about by people on this and other servers of the decentralized network right now.' />
|
||||
<FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.' />
|
||||
</DismissableBanner>
|
||||
);
|
||||
|
||||
|
@ -11,9 +11,10 @@ import { debounce } from 'lodash';
|
||||
import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends';
|
||||
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||
import StatusList from 'mastodon/components/status_list';
|
||||
import { getStatusList } from 'mastodon/selectors';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
statusIds: state.getIn(['status_lists', 'trending', 'items']),
|
||||
statusIds: getStatusList(state, 'trending'),
|
||||
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'trending', 'next']),
|
||||
});
|
||||
@ -46,7 +47,7 @@ class Statuses extends PureComponent {
|
||||
return (
|
||||
<>
|
||||
<DismissableBanner id='explore/statuses'>
|
||||
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These posts from this and other servers in the decentralized network are gaining traction on this server right now.' />
|
||||
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.' />
|
||||
</DismissableBanner>
|
||||
|
||||
<StatusList
|
||||
|
@ -34,7 +34,7 @@ class Tags extends PureComponent {
|
||||
|
||||
const banner = (
|
||||
<DismissableBanner id='explore/tags'>
|
||||
<FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These hashtags are gaining traction among people on this and other servers of the decentralized network right now.' />
|
||||
<FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.' />
|
||||
</DismissableBanner>
|
||||
);
|
||||
|
||||
|
@ -15,13 +15,14 @@ import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'mastodon/acti
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import StatusList from 'mastodon/components/status_list';
|
||||
import Column from 'mastodon/features/ui/components/column';
|
||||
import { getStatusList } from 'mastodon/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
||||
statusIds: getStatusList(state, 'favourites'),
|
||||
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
||||
});
|
||||
|
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import background from 'mastodon/../images/friends-cropped.png';
|
||||
|
||||
import DismissableBanner from 'mastodon/components/dismissable_banner';
|
||||
|
||||
|
||||
export const ExplorePrompt = () => (
|
||||
<DismissableBanner id='home.explore_prompt'>
|
||||
<img src={background} alt='' className='dismissable-banner__background-image' />
|
||||
|
||||
<h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
|
||||
<p><FormattedMessage id='home.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. It's looking pretty quiet right now, so how about:" /></p>
|
||||
|
||||
<div className='dismissable-banner__message__actions'>
|
||||
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
|
||||
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
|
||||
</div>
|
||||
</DismissableBanner>
|
||||
);
|
@ -5,14 +5,16 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements';
|
||||
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
|
||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
import { expandHomeTimeline } from '../../actions/timelines';
|
||||
@ -20,6 +22,7 @@ import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
|
||||
import { ExplorePrompt } from './components/explore_prompt';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -28,12 +31,33 @@ const messages = defineMessages({
|
||||
hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' },
|
||||
});
|
||||
|
||||
const getHomeFeedSpeed = createSelector([
|
||||
state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
|
||||
state => state.get('statuses'),
|
||||
], (statusIds, statusMap) => {
|
||||
const statuses = statusIds.map(id => statusMap.get(id)).filter(status => status.get('account') !== me).take(20);
|
||||
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
|
||||
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
|
||||
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
||||
|
||||
return {
|
||||
gap: averageGap,
|
||||
newest,
|
||||
};
|
||||
});
|
||||
|
||||
const homeTooSlow = createSelector(getHomeFeedSpeed, speed =>
|
||||
speed.gap > (30 * 60) // If the average gap between posts is more than 20 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']),
|
||||
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
|
||||
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
|
||||
showAnnouncements: state.getIn(['announcements', 'show']),
|
||||
tooSlow: homeTooSlow(state),
|
||||
});
|
||||
|
||||
class HomeTimeline extends PureComponent {
|
||||
@ -52,6 +76,7 @@ class HomeTimeline extends PureComponent {
|
||||
hasAnnouncements: PropTypes.bool,
|
||||
unreadAnnouncements: PropTypes.number,
|
||||
showAnnouncements: PropTypes.bool,
|
||||
tooSlow: PropTypes.bool,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
@ -121,11 +146,11 @@ class HomeTimeline extends PureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
||||
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
||||
const pinned = !!columnId;
|
||||
const { signedIn } = this.context.identity;
|
||||
|
||||
let announcementsButton = null;
|
||||
let announcementsButton, banner;
|
||||
|
||||
if (hasAnnouncements) {
|
||||
announcementsButton = (
|
||||
@ -141,6 +166,10 @@ class HomeTimeline extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (tooSlow) {
|
||||
banner = <ExplorePrompt />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
@ -160,11 +189,13 @@ class HomeTimeline extends PureComponent {
|
||||
|
||||
{signedIn ? (
|
||||
<StatusListContainer
|
||||
prepend={banner}
|
||||
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. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />}
|
||||
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up.' />}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
) : <NotSignedInIndicator />}
|
||||
|
@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
||||
import { Check } from 'mastodon/components/check';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import ArrowSmallRight from './arrow_small_right';
|
||||
|
||||
const Step = ({ label, description, icon, completed, onClick, href }) => {
|
||||
const content = (
|
||||
<>
|
||||
@ -15,11 +17,9 @@ const Step = ({ label, description, icon, completed, onClick, href }) => {
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
|
||||
{completed && (
|
||||
<div className='onboarding__steps__item__progress'>
|
||||
<Check />
|
||||
</div>
|
||||
)}
|
||||
<div className={completed ? 'onboarding__steps__item__progress' : 'onboarding__steps__item__go'}>
|
||||
{completed ? <Check /> : <ArrowSmallRight />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -12,20 +12,11 @@ import Column from 'mastodon/components/column';
|
||||
import ColumnBackButton from 'mastodon/components/column_back_button';
|
||||
import { EmptyAccount } from 'mastodon/components/empty_account';
|
||||
import Account from 'mastodon/containers/account_container';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { makeGetAccount } from 'mastodon/selectors';
|
||||
|
||||
import ProgressIndicator from './components/progress_indicator';
|
||||
|
||||
const mapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
return state => ({
|
||||
account: getAccount(state, me),
|
||||
suggestions: state.getIn(['suggestions', 'items']),
|
||||
isLoading: state.getIn(['suggestions', 'isLoading']),
|
||||
});
|
||||
};
|
||||
const mapStateToProps = state => ({
|
||||
suggestions: state.getIn(['suggestions', 'items']),
|
||||
isLoading: state.getIn(['suggestions', 'isLoading']),
|
||||
});
|
||||
|
||||
class Follows extends PureComponent {
|
||||
|
||||
@ -33,7 +24,6 @@ class Follows extends PureComponent {
|
||||
onBack: PropTypes.func,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
account: ImmutablePropTypes.map,
|
||||
isLoading: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
@ -49,7 +39,7 @@ class Follows extends PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { onBack, isLoading, suggestions, account, multiColumn } = this.props;
|
||||
const { onBack, isLoading, suggestions, multiColumn } = this.props;
|
||||
|
||||
let loadedContent;
|
||||
|
||||
@ -58,7 +48,7 @@ class Follows extends PureComponent {
|
||||
} else if (suggestions.isEmpty()) {
|
||||
loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
|
||||
} else {
|
||||
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} />);
|
||||
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -71,8 +61,6 @@ class Follows extends PureComponent {
|
||||
<p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
|
||||
</div>
|
||||
|
||||
<ProgressIndicator steps={7} completed={account.get('following_count') * 1} />
|
||||
|
||||
<div className='follow-recommendations'>
|
||||
{loadedContent}
|
||||
</div>
|
||||
|
@ -19,6 +19,7 @@ import { closeOnboarding } from 'mastodon/actions/onboarding';
|
||||
import Column from 'mastodon/features/ui/components/column';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { makeGetAccount } from 'mastodon/selectors';
|
||||
import { assetHost } from 'mastodon/utils/config';
|
||||
|
||||
import ArrowSmallRight from './components/arrow_small_right';
|
||||
import Step from './components/step';
|
||||
@ -122,21 +123,22 @@ class Onboarding extends ImmutablePureComponent {
|
||||
<div className='onboarding__steps'>
|
||||
<Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
|
||||
<Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
|
||||
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' />} />
|
||||
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
|
||||
<Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' |