Skip to content

Commit

Permalink
Redesign public hashtag pages (mastodon#5237)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gargron authored and cobodo committed Oct 20, 2017
1 parent 7361e0f commit 4cf5fa5
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 62 deletions.
30 changes: 25 additions & 5 deletions app/controllers/tags_controller.rb
@@ -1,17 +1,22 @@
# frozen_string_literal: true

class TagsController < ApplicationController
layout 'public'
before_action :set_body_classes
before_action :set_instance_presenter

def show
@tag = Tag.find_by!(name: params[:id].downcase)
@statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
@statuses = cache_collection(@statuses, Status)
@tag = Tag.find_by!(name: params[:id].downcase)

respond_to do |format|
format.html
format.html do
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end

format.json do
@statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
@statuses = cache_collection(@statuses, Status)

render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
Expand All @@ -22,6 +27,14 @@ def show

private

def set_body_classes
@body_classes = 'tag-body'
end

def set_instance_presenter
@instance_presenter = InstancePresenter.new
end

def collection_presenter
ActivityPub::CollectionPresenter.new(
id: tag_url(@tag),
Expand All @@ -30,4 +43,11 @@ def collection_presenter
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
)
end

def initial_state_params
{
settings: {},
token: current_session&.token,
}
end
end
14 changes: 12 additions & 2 deletions app/javascript/mastodon/containers/timeline_container.js
Expand Up @@ -6,6 +6,7 @@ import { hydrateStore } from '../actions/store';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import PublicTimeline from '../features/standalone/public_timeline';
import HashtagTimeline from '../features/standalone/hashtag_timeline';

const { localeData, messages } = getLocale();
addLocaleData(localeData);
Expand All @@ -22,15 +23,24 @@ export default class TimelineContainer extends React.PureComponent {

static propTypes = {
locale: PropTypes.string.isRequired,
hashtag: PropTypes.string,
};

render () {
const { locale } = this.props;
const { locale, hashtag } = this.props;

let timeline;

if (hashtag) {
timeline = <HashtagTimeline hashtag={hashtag} />;
} else {
timeline = <PublicTimeline />;
}

return (
<IntlProvider locale={locale} messages={messages}>
<Provider store={store}>
<PublicTimeline />
{timeline}
</Provider>
</IntlProvider>
);
Expand Down
@@ -0,0 +1,70 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from '../../ui/containers/status_list_container';
import {
refreshHashtagTimeline,
expandHashtagTimeline,
} from '../../../actions/timelines';
import Column from '../../../components/column';
import ColumnHeader from '../../../components/column_header';

@connect()
export default class HashtagTimeline extends React.PureComponent {

static propTypes = {
dispatch: PropTypes.func.isRequired,
hashtag: PropTypes.string.isRequired,
};

handleHeaderClick = () => {
this.column.scrollTop();
}

setRef = c => {
this.column = c;
}

componentDidMount () {
const { dispatch, hashtag } = this.props;

dispatch(refreshHashtagTimeline(hashtag));

this.polling = setInterval(() => {
dispatch(refreshHashtagTimeline(hashtag));
}, 10000);
}

componentWillUnmount () {
if (typeof this.polling !== 'undefined') {
clearInterval(this.polling);
this.polling = null;
}
}

handleLoadMore = () => {
this.props.dispatch(expandHashtagTimeline(this.props.hashtag));
}

render () {
const { hashtag } = this.props;

return (
<Column ref={this.setRef}>
<ColumnHeader
icon='hashtag'
title={hashtag}
onClick={this.handleHeaderClick}
/>

<StatusListContainer
trackScroll={false}
scrollKey='standalone_hashtag_timeline'
timelineId={`hashtag:${hashtag}`}
loadMore={this.handleLoadMore}
/>
</Column>
);
}

}
6 changes: 3 additions & 3 deletions app/javascript/packs/about.js
Expand Up @@ -4,9 +4,9 @@ require.context('../images/', true);

function loaded() {
const TimelineContainer = require('../mastodon/containers/timeline_container').default;
const React = require('react');
const ReactDOM = require('react-dom');
const mountNode = document.getElementById('mastodon-timeline');
const React = require('react');
const ReactDOM = require('react-dom');
const mountNode = document.getElementById('mastodon-timeline');

if (mountNode !== null) {
const props = JSON.parse(mountNode.getAttribute('data-props'));
Expand Down
91 changes: 91 additions & 0 deletions app/javascript/styles/about.scss
Expand Up @@ -481,6 +481,7 @@
flex: 0 0 auto;
background: $ui-base-color;
overflow: hidden;
border-radius: 4px;
box-shadow: 0 0 6px rgba($black, 0.1);

.column-header {
Expand Down Expand Up @@ -703,8 +704,98 @@
.features #mastodon-timeline {
height: 70vh;
width: 100%;
min-width: 330px;
margin-bottom: 50px;

.column {
width: 100%;
}
}
}

.cta {
margin: 20px;
}

&.tag-page {
.brand {
padding-top: 20px;
margin-bottom: 20px;

img {
height: 48px;
width: auto;
}
}

.container {
max-width: 690px;
}

.cta {
margin: 40px 0;
margin-bottom: 80px;

.button {
margin-right: 4px;
}
}

.about-mastodon {
max-width: 330px;

p {
strong {
color: $ui-secondary-color;
font-weight: 700;
}
}
}

@media screen and (max-width: 675px) {
.container {
display: flex;
flex-direction: column;
}

.features {
padding: 20px 0;
}

.about-mastodon {
order: 1;
flex: 0 0 auto;
max-width: 100%;
}

#mastodon-timeline {
order: 2;
flex: 0 0 auto;
height: 60vh;
}

.cta {
margin: 20px 0;
margin-bottom: 30px;
}

.features-list {
display: none;
}

.stripe {
display: none;
}
}
}

.stripe {
width: 100%;
height: 360px;
overflow: hidden;
background: darken($ui-base-color, 4%);
position: absolute;
z-index: -1;
}
}

Expand Down
5 changes: 5 additions & 0 deletions app/javascript/styles/basics.scss
Expand Up @@ -42,6 +42,11 @@ body {
padding-bottom: 0;
}

&.tag-body {
background: darken($ui-base-color, 8%);
padding-bottom: 0;
}

&.embed {
background: transparent;
margin: 0;
Expand Down
1 change: 1 addition & 0 deletions app/javascript/styles/components.scss
Expand Up @@ -66,6 +66,7 @@
text-transform: none;
background: transparent;
padding: 3px 15px;
border-radius: 4px;
border: 1px solid $ui-primary-color;

&:active,
Expand Down
2 changes: 1 addition & 1 deletion app/views/about/show.html.haml
Expand Up @@ -62,7 +62,7 @@
.about-mastodon
%h3= t 'about.what_is_mastodon'
%p= t 'about.about_mastodon_html'
%a.button.button-secondary{ href: 'https://joinmastodon.org/' }= t 'about.learn_more'
= link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-secondary'
= render 'features'
.footer-links
.container
Expand Down
6 changes: 6 additions & 0 deletions app/views/tags/_og.html.haml
@@ -0,0 +1,6 @@
= opengraph 'og:site_name', t('about.hosted_on', domain: site_hostname)
= opengraph 'og:url', tag_url(@tag)
= opengraph 'og:type', 'website'
= opengraph 'og:title', "##{@tag.name}"
= opengraph 'og:description', t('about.about_hashtag_html', hashtag: @tag.name)
= opengraph 'twitter:card', 'summary'
47 changes: 33 additions & 14 deletions app/views/tags/show.html.haml
@@ -1,19 +1,38 @@
- content_for :page_title do
= "##{@tag.name}"

.compact-header
%h1<
= link_to site_title, root_path
%br
%small ##{@tag.name}
- content_for :header_tags do
%script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
= javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous'
= render 'og'

- if @statuses.empty?
.accounts-grid
= render partial: 'accounts/nothing_here'
- else
.activity-stream.h-feed
= render partial: 'stream_entries/status', collection: @statuses, as: :status
.landing-page.tag-page
.stripe
.features
.container
#mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } }

- if @statuses.size == 20
.pagination
= link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next', rel: 'next'
.about-mastodon
.brand
= link_to root_url do
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'

%p= t 'about.about_hashtag_html', hashtag: @tag.name

.cta
= link_to t('auth.login'), new_user_session_path, class: 'button button-secondary'
= link_to t('about.learn_more'), root_url, class: 'button button-alternative'

.features-list
.features-list__row
.text
%h6= t 'about.features.not_a_product_title'
= t 'about.features.not_a_product_body'
.visual
= fa_icon 'fw users'
.features-list__row
.text
%h6= t 'about.features.humane_approach_title'
= t 'about.features.humane_approach_body'
.visual
= fa_icon 'fw leaf'
1 change: 1 addition & 0 deletions config/locales/en.yml
Expand Up @@ -2,6 +2,7 @@
en:
about:
about_mastodon_html: Mastodon is a social network based on open web protocols and free, open-source software. It is decentralized like e-mail.
about_hashtag_html: These are public toots tagged with <strong>#%{hashtag}</strong>. You can interact with them if you have an account anywhere in the fediverse.
about_this: About
closed_registrations: Registrations are currently closed on this instance. However! You can find a different instance to make an account on and get access to the very same network from there.
contact: Contact
Expand Down

0 comments on commit 4cf5fa5

Please sign in to comment.