Redesign the landing page, mount public timeline on it (#4122)

* Redesign the landing page, mount public timeline on it

* Adjust the standalone mounted component to the lacking of router

* Adjust auth layout pages to new design

* Fix tests

* Standalone public timeline polling every 5 seconds

* Remove now obsolete translations

* Add responsive design for new landing page

* Address reviews

* Add floating clouds behind frontpage form

* Use access token from public page when available

* Fix mentions and hashtags links, cursor on status content in standalone mode

* Add footer link to source code

* Fix errors on pages that don't embed the component, use classnames

* Fix tests

* Change anonymous autoPlayGif default to false

* When gif autoplay is disabled, hover to play

* Add option to hide the timeline preview

* Slightly improve alt layout

* Add elephant friend to new frontpage

* Display "back to mastodon" in place of "login" when logged in on frontpage

* Change polling time to 3s
This commit is contained in:
Eugen Rochko
2017-07-11 15:27:59 +02:00
committed by GitHub
parent 8784bd79d0
commit e19eefe219
68 changed files with 959 additions and 658 deletions

View File

@ -4,7 +4,10 @@ class AboutController < ApplicationController
before_action :set_body_classes
before_action :set_instance_presenter, only: [:show, :more, :terms]
def show; end
def show
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end
def more; end
@ -15,6 +18,7 @@ class AboutController < ApplicationController
def new_user
User.new.tap(&:build_account)
end
helper_method :new_user
def set_instance_presenter
@ -24,4 +28,11 @@ class AboutController < ApplicationController
def set_body_classes
@body_classes = 'about-body'
end
def initial_state_params
{
settings: {},
token: current_session&.token,
}
end
end

View File

@ -11,8 +11,15 @@ module Admin
site_terms
open_registrations
closed_registrations_message
open_deletion
timeline_preview
).freeze
BOOLEAN_SETTINGS = %w(
open_registrations
open_deletion
timeline_preview
).freeze
BOOLEAN_SETTINGS = %w(open_registrations).freeze
def edit
@settings = Setting.all_as_records

View File

@ -15,12 +15,16 @@ class HomeController < ApplicationController
end
def set_initial_state_json
state = InitialStatePresenter.new(settings: Web::Setting.find_by(user: current_user)&.data || {},
current_account: current_account,
token: current_session.token,
admin: Account.find_local(Setting.site_contact_username))
serializable_resource = ActiveModelSerializers::SerializableResource.new(state, serializer: InitialStateSerializer)
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end
def initial_state_params
{
settings: Web::Setting.find_by(user: current_user)&.data || {},
current_account: current_account,
token: current_session.token,
admin: Account.find_local(Setting.site_contact_username),
}
end
end

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#189efc"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -14,6 +14,7 @@ export default class DropdownMenu extends React.PureComponent {
size: PropTypes.number.isRequired,
direction: PropTypes.string,
ariaLabel: PropTypes.string,
disabled: PropTypes.bool,
};
static defaultProps = {
@ -68,9 +69,19 @@ export default class DropdownMenu extends React.PureComponent {
}
render () {
const { icon, items, size, direction, ariaLabel } = this.props;
const { expanded } = this.state;
const { icon, items, size, direction, ariaLabel, disabled } = this.props;
const { expanded } = this.state;
const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right';
const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` };
const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`;
if (disabled) {
return (
<div className='icon-button disabled' style={iconStyle} aria-label={ariaLabel}>
<i className={iconClassname} aria-hidden />
</div>
);
}
const dropdownItems = expanded && (
<ul className='dropdown__content-list'>
@ -80,8 +91,8 @@ export default class DropdownMenu extends React.PureComponent {
return (
<Dropdown ref={this.setRef} onShow={this.handleShow} onHide={this.handleHide}>
<DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }} aria-label={ariaLabel}>
<i className={`fa fa-fw fa-${icon} dropdown__icon`} aria-hidden />
<DropdownTrigger className='icon-button' style={iconStyle} aria-label={ariaLabel}>
<i className={iconClassname} aria-hidden />
</DropdownTrigger>
<DropdownContent className={directionClass}>

View File

@ -11,18 +11,44 @@ const messages = defineMessages({
class Item extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
attachment: ImmutablePropTypes.map.isRequired,
index: PropTypes.number.isRequired,
size: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired,
autoPlayGif: PropTypes.bool.isRequired,
autoPlayGif: PropTypes.bool,
};
static defaultProps = {
autoPlayGif: false,
};
handleMouseEnter = (e) => {
if (this.hoverToPlay()) {
e.target.play();
}
}
handleMouseLeave = (e) => {
if (this.hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
}
}
hoverToPlay () {
const { attachment, autoPlayGif } = this.props;
return !autoPlayGif && attachment.get('type') === 'gifv';
}
handleClick = (e) => {
const { index, onClick } = this.props;
if (e.button === 0) {
if (this.context.router && e.button === 0) {
e.preventDefault();
onClick(index);
}
@ -116,6 +142,8 @@ class Item extends React.PureComponent {
role='application'
src={attachment.get('url')}
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlay}
loop
muted
@ -144,7 +172,11 @@ export default class MediaGallery extends React.PureComponent {
height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
autoPlayGif: PropTypes.bool.isRequired,
autoPlayGif: PropTypes.bool,
};
static defaultProps = {
autoPlayGif: false,
};
state = {

View File

@ -15,7 +15,7 @@ export default class Permalink extends React.PureComponent {
};
handleClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(this.props.to);
}
@ -25,7 +25,7 @@ export default class Permalink extends React.PureComponent {
const { href, children, className, ...other } = this.props;
return (
<a href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
<a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
{children}
</a>
);

View File

@ -140,12 +140,16 @@ export default class Status extends ImmutablePureComponent {
}
handleClick = () => {
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
}
handleAccountClick = (e) => {
if (e.button === 0) {
if (this.context.router && e.button === 0) {
const id = Number(e.currentTarget.getAttribute('data-id'));
e.preventDefault();
this.context.router.history.push(`/accounts/${id}`);
@ -236,7 +240,7 @@ export default class Status extends ImmutablePureComponent {
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
<a onClick={this.handleAccountClick} target='_blank' data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'>
{statusAvatar}
</div>

View File

@ -40,7 +40,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
onBlock: PropTypes.func,
onReport: PropTypes.func,
onMuteConversation: PropTypes.func,
me: PropTypes.number.isRequired,
me: PropTypes.number,
withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@ -97,6 +97,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
const { status, me, intl, withDismiss } = this.props;
const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
const mutingConversation = status.get('muted');
const anonymousAccess = !me;
let menu = [];
let reblogIcon = 'retweet';
@ -137,12 +138,12 @@ export default class StatusActionBar extends ImmutablePureComponent {
return (
<div className='status__action-bar'>
<IconButton className='status__action-bar-button' title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
<IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
<div className='status__action-bar-dropdown'>
<DropdownMenu items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
<DropdownMenu disabled={anonymousAccess} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
</div>
</div>
);

View File

@ -6,6 +6,7 @@ import emojify from '../emoji';
import { isRtl } from '../rtl';
import { FormattedMessage } from 'react-intl';
import Permalink from './permalink';
import classnames from 'classnames';
export default class StatusContent extends React.PureComponent {
@ -43,10 +44,11 @@ export default class StatusContent extends React.PureComponent {
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
} else {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
link.setAttribute('title', link.href);
}
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
}
}
@ -59,7 +61,7 @@ export default class StatusContent extends React.PureComponent {
}
onMentionClick = (mention, e) => {
if (e.button === 0) {
if (this.context.router && e.button === 0) {
e.preventDefault();
this.context.router.history.push(`/accounts/${mention.get('id')}`);
}
@ -68,7 +70,7 @@ export default class StatusContent extends React.PureComponent {
onHashtagClick = (hashtag, e) => {
hashtag = hashtag.replace(/^#/, '').toLowerCase();
if (e.button === 0) {
if (this.context.router && e.button === 0) {
e.preventDefault();
this.context.router.history.push(`/timelines/tag/${hashtag}`);
}
@ -120,6 +122,9 @@ export default class StatusContent extends React.PureComponent {
const content = { __html: emojify(status.get('content')) };
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
const directionStyle = { direction: 'ltr' };
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
});
if (isRtl(status.get('search_index'))) {
directionStyle.direction = 'rtl';
@ -141,7 +146,7 @@ export default class StatusContent extends React.PureComponent {
}
return (
<div className='status__content status__content--with-action' ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} />
{' '}
@ -157,7 +162,7 @@ export default class StatusContent extends React.PureComponent {
return (
<div
ref={this.setRef}
className='status__content status__content--with-action'
className={classNames}
style={directionStyle}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}

View File

@ -14,6 +14,10 @@ const messages = defineMessages({
@injectIntl
export default class VideoPlayer extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
width: PropTypes.number,
@ -119,11 +123,15 @@ export default class VideoPlayer extends React.PureComponent {
</div>
);
let expandButton = (
<div className='status__video-player-expand'>
<IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
</div>
);
let expandButton = '';
if (this.context.router) {
expandButton = (
<div className='status__video-player-expand'>
<IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
</div>
);
}
let muteButton = '';
@ -138,7 +146,7 @@ export default class VideoPlayer extends React.PureComponent {
if (!this.state.visible) {
if (sensitive) {
return (
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton}
<span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
@ -146,7 +154,7 @@ export default class VideoPlayer extends React.PureComponent {
);
} else {
return (
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton}
<span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>

View File

@ -0,0 +1,39 @@
import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
import { hydrateStore } from '../actions/store';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import PublicTimeline from '../features/standalone/public_timeline';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
const store = configureStore();
const initialStateContainer = document.getElementById('initial-state');
if (initialStateContainer !== null) {
const initialState = JSON.parse(initialStateContainer.textContent);
store.dispatch(hydrateStore(initialState));
}
export default class TimelineContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
render () {
const { locale } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
<Provider store={store}>
<PublicTimeline />
</Provider>
</IntlProvider>
);
}
}

View File

@ -0,0 +1,76 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from '../../ui/containers/status_list_container';
import {
refreshPublicTimeline,
expandPublicTimeline,
} from '../../../actions/timelines';
import Column from '../../../components/column';
import ColumnHeader from '../../../components/column_header';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
});
@connect()
@injectIntl
export default class PublicTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
componentDidMount () {
const { dispatch } = this.props;
dispatch(refreshPublicTimeline());
this.polling = setInterval(() => {
dispatch(refreshPublicTimeline());
}, 3000);
}
componentWillUnmount () {
if (typeof this.polling !== 'undefined') {
clearInterval(this.polling);
this.polling = null;
}
}
handleLoadMore = () => {
this.props.dispatch(expandPublicTimeline());
}
render () {
const { intl } = this.props;
return (
<Column ref={this.setRef}>
<ColumnHeader
icon='globe'
title={intl.formatMessage(messages.title)}
onClick={this.handleHeaderClick}
/>
<StatusListContainer
timelineId='public'
loadMore={this.handleLoadMore}
scrollKey='standalone_public_timeline'
trackScroll={false}
/>
</Column>
);
}
}

View File

@ -4,6 +4,9 @@ import { delegate } from 'rails-ujs';
import emojify from '../mastodon/emoji';
import { getLocale } from '../mastodon/locales';
import loadPolyfills from '../mastodon/load_polyfills';
import TimelineContainer from '../mastodon/containers/timeline_container';
import React from 'react';
import ReactDOM from 'react-dom';
require.context('../images/', true);
@ -36,6 +39,13 @@ function loaded() {
const datetime = new Date(content.getAttribute('datetime'));
content.textContent = relativeFormat.format(datetime);;
});
const mountNode = document.getElementById('mastodon-timeline');
if (mountNode !== null) {
const props = JSON.parse(mountNode.getAttribute('data-props'));
ReactDOM.render(<TimelineContainer {...props} />, mountNode);
}
}
function main() {

View File

@ -116,10 +116,6 @@
.wrapper {
padding: 20px;
}
.features-list {
display: block;
}
}
}
@ -301,80 +297,438 @@
}
}
.features-list {
.features-list__row {
display: flex;
margin-bottom: 20px;
padding: 10px 0;
justify-content: space-between;
.features-list__column {
flex: 1 1 0;
&:first-child {
padding-top: 0;
}
ul {
list-style: none;
.visual {
flex: 0 0 auto;
display: flex;
align-items: center;
margin-left: 15px;
.fa {
display: block;
color: $ui-primary-color;
font-size: 48px;
}
}
li {
margin: 0;
.text {
font-size: 16px;
line-height: 30px;
color: lighten($ui-base-color, 26%);
h6 {
font-weight: 500;
color: $ui-primary-color;
}
}
}
.screenshot-with-signup {
display: flex;
margin-bottom: 20px;
.landing-page {
.header-wrapper {
padding-top: 15px;
background: $ui-base-color;
background: linear-gradient(150deg, lighten($ui-base-color, 8%), $ui-base-color);
position: relative;
.mascot {
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
img {
display: block;
.mascot-container {
max-width: 800px;
margin: 0 auto;
max-width: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
}
.mascot {
position: absolute;
bottom: -14px;
width: auto;
height: auto;
left: 60px;
z-index: 3;
}
}
.simple_form,
.closed-registrations-message {
width: 300px;
flex: 0 0 auto;
background: rgba(darken($ui-base-color, 7%), 0.5);
padding: 14px;
border-radius: 4px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.4);
p,
li {
font: inherit;
font-weight: inherit;
margin-bottom: 0;
}
.actions {
margin-bottom: 0;
.header {
line-height: 30px;
overflow: hidden;
.container {
display: flex;
justify-content: space-between;
}
.info {
text-align: center;
.hero {
margin-top: 50px;
align-items: center;
position: relative;
.floats {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
img {
position: absolute;
transition: all 0.1s linear;
animation-name: floating;
animation-duration: 1.7s;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-timing-function: linear;
z-index: 2;
}
.float-1 {
height: 170px;
right: -120px;
bottom: 0;
}
.float-2 {
height: 100px;
right: 210px;
bottom: 0;
animation-delay: 0.2s;
}
.float-3 {
height: 140px;
right: 110px;
top: -30px;
animation-delay: 0.1s;
}
}
.simple_form,
.closed-registrations-message {
background: darken($ui-base-color, 4%);
width: 280px;
padding: 15px 20px;
border-radius: 4px 4px 0 0;
line-height: initial;
position: relative;
z-index: 4;
.actions {
margin-bottom: 0;
button,
.button,
.block-button {
margin-bottom: 0;
}
}
}
.heading {
position: relative;
z-index: 4;
padding-bottom: 150px;
}
.closed-registrations-message {
min-height: 330px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
ul {
list-style: none;
margin: 0;
li {
display: inline-block;
vertical-align: bottom;
margin: 0;
&:first-child a {
padding-left: 0;
}
&:last-child a {
padding-right: 0;
}
}
}
.links {
position: relative;
z-index: 4;
a {
color: $ui-secondary-color;
display: flex;
justify-content: center;
align-items: center;
color: $ui-primary-color;
text-decoration: none;
padding: 12px 16px;
line-height: 32px;
font-family: 'mastodon-font-display', sans-serif;
font-weight: 500;
font-size: 14px;
&:hover {
color: $ui-secondary-color;
}
}
.brand {
a {
padding-left: 0;
color: $white;
}
img {
width: 32px;
height: 32px;
margin-right: 10px;
}
}
}
}
@media screen and (max-width: 625px) {
.mascot {
display: none;
.container {
width: 100%;
box-sizing: border-box;
max-width: 800px;
margin: 0 auto;
}
.wrapper {
max-width: 800px;
margin: 0 auto;
padding: 0;
}
.learn-more-cta {
background: darken($ui-base-color, 4%);
padding: 50px 0;
}
h3 {
font-family: 'mastodon-font-display', sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $ui-primary-color;
}
p {
font-size: 16px;
line-height: 30px;
color: lighten($ui-base-color, 26%);
}
.features {
padding: 50px 0;
.container {
display: flex;
}
}
#mastodon-timeline {
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
font-family: 'mastodon-font-sans-serif', sans-serif;
font-size: 13px;
line-height: 18px;
font-weight: 400;
color: $primary-text-color;
width: 330px;
margin-right: 30px;
flex: 0 0 auto;
background: $ui-base-color;
overflow: hidden;
box-shadow: 0 0 6px rgba($black, 0.1);
.column {
padding: 0;
border-radius: 4px;
overflow: hidden;
height: 100%;
}
.simple_form,
.closed-registrations-message {
flex: auto;
.scrollable {
height: 400px;
}
p {
font-size: inherit;
line-height: inherit;
font-weight: inherit;
color: $primary-text-color;
a {
color: $ui-secondary-color;
text-decoration: none;
}
}
}
.about-mastodon {
max-width: 675px;
p {
margin-bottom: 20px;
}
.features-list {
margin-top: 20px;
}
}
em {
display: inline;
margin: 0;
padding: 0;
font-weight: 500;
background: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: $ui-primary-color;
}
h1 {
font-family: 'mastodon-font-display', sans-serif;
font-size: 26px;
line-height: 30px;
margin-bottom: 0;
font-weight: 500;
color: $ui-secondary-color;
small {
font-family: 'mastodon-font-sans-serif', sans-serif;
display: block;
font-size: 18px;
font-weight: 400;
color: lighten($ui-base-color, 26%);
}
}
.footer-links {
padding-bottom: 50px;
text-align: right;
color: lighten($ui-base-color, 26%);
p {
font-size: 14px;
}
a {
color: inherit;
text-decoration: underline;
}
}
@media screen and (max-width: 800px) {
.container {
padding: 0 20px;
}
.header-wrapper .mascot {
left: 20px;
}
}
@media screen and (max-width: 689px) {
.header-wrapper .mascot {
display: none;
}
}
@media screen and (max-width: 675px) {
.header-wrapper {
padding-top: 0;
}
.header .container,
.features .container {
display: block;
}
.links {
padding-top: 15px;
background: darken($ui-base-color, 4%);
}
.header {
padding-top: 0;
.hero {
margin-top: 30px;
padding: 0;
.heading {
padding-bottom: 20px;
}
}
.floats {
display: none;
}
.heading,
.nav {
text-align: center;
}
.heading h1 {
padding: 30px 0;
}
.hero {
.simple_form,
.closed-registrations-message {
background: darken($ui-base-color, 8%);
width: 100%;
border-radius: 0;
box-sizing: border-box;
}
}
}
#mastodon-timeline {
height: 70vh;
width: 100%;
margin-bottom: 50px;
}
}
}
.closed-registrations-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
@keyframes floating {
from {
transform: translate(0, 0);
}
65% {
transform: translate(0, 4px);
}
to {
transform: translate(0, -0);
}
}

View File

@ -1,6 +1,6 @@
body {
font-family: 'mastodon-font-sans-serif', sans-serif;
background: $ui-base-color url('../images/background-photo.jpg');
background: $ui-base-color;
background-size: cover;
background-attachment: fixed;
font-size: 13px;
@ -22,6 +22,11 @@ body {
background: $ui-base-color;
}
&.about-body {
background: darken($ui-base-color, 8%);
padding-bottom: 0;
}
&.embed {
background: transparent;
margin: 0;

File diff suppressed because one or more lines are too long

View File

@ -42,8 +42,38 @@
cursor: default;
}
&.button-alternative {
font-size: 16px;
line-height: 36px;
height: auto;
color: $ui-base-color;
background: $ui-primary-color;
text-transform: none;
padding: 4px 16px;
&:active,
&:focus,
&:hover {
background-color: lighten($ui-primary-color, 4%);
}
}
&.button-secondary {
//
font-size: 16px;
line-height: 36px;
height: auto;
color: $ui-primary-color;
text-transform: none;
background: transparent;
padding: 3px 15px;
border: 1px solid $ui-primary-color;
&:active,
&:focus,
&:hover {
border-color: lighten($ui-primary-color, 4%);
color: lighten($ui-primary-color, 4%);
}
}
&.button--block {

View File

@ -10,52 +10,36 @@
}
.logo-container {
max-width: 400px;
margin: 100px auto;
margin-bottom: 0;
cursor: default;
margin-bottom: 50px;
@media screen and (max-width: 360px) {
margin: 30px auto;
}
h1 {
display: block;
text-align: center;
color: $primary-text-color;
font-size: 48px;
font-weight: 500;
display: flex;
justify-content: center;
align-items: center;
img {
display: block;
margin: 20px auto;
width: 180px;
height: 180px;
width: 32px;
height: 32px;
margin-right: 10px;
}
a {
color: inherit;
display: flex;
justify-content: center;
align-items: center;
color: $primary-text-color;
text-decoration: none;
outline: 0;
img {
opacity: 0.8;
transition: opacity 0.8s ease;
}
&:hover {
img {
opacity: 1;
transition-duration: 0.2s;
}
}
}
small {
display: block;
font-size: 12px;
font-weight: 400;
font-family: 'mastodon-font-monospace', monospace;
padding: 12px 16px;
line-height: 32px;
font-family: 'mastodon-font-display', sans-serif;
font-weight: 500;
font-size: 14px;
}
}
}

View File

@ -7,3 +7,11 @@
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'mastodon-font-display';
src: local('Montserrat'),
url('../fonts/montserrat/Montserrat-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}

View File

@ -24,6 +24,20 @@ code {
p.hint {
margin-bottom: 15px;
color: lighten($ui-base-color, 32%);
&.subtle-hint {
text-align: center;
font-size: 12px;
line-height: 18px;
margin-top: 15px;
margin-bottom: 0;
color: lighten($ui-base-color, 26%);
a {
color: $ui-primary-color;
}
}
}
strong {
@ -197,8 +211,6 @@ code {
&:active,
&:focus {
position: relative;
top: 1px;
background-color: darken($ui-highlight-color, 5%);
}
@ -219,6 +231,27 @@ code {
select {
font-size: 16px;
}
.input-with-append {
position: relative;
.input input {
padding-right: 127px;
}
.append {
position: absolute;
right: 0;
top: 0;
padding: 7px 4px;
padding-bottom: 9px;
font-size: 16px;
color: lighten($ui-base-color, 26%);
font-family: inherit;
pointer-events: none;
cursor: default;
}
}
}
.flash-message {
@ -240,7 +273,7 @@ code {
text-align: center;
a {
color: $primary-text-color;
color: $ui-primary-color;
text-decoration: none;
&:hover {

View File

@ -5,6 +5,7 @@ class InstancePresenter
:closed_registrations_message,
:site_contact_email,
:open_registrations,
:site_title,
:site_description,
:site_extended_description,
:site_terms,

View File

@ -5,32 +5,41 @@ class InitialStateSerializer < ActiveModel::Serializer
:media_attachments, :settings
def meta
{
store = {
streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
access_token: object.token,
locale: I18n.locale,
domain: Rails.configuration.x.local_domain,
me: object.current_account.id,
admin: object.admin&.id,
boost_modal: object.current_account.user.setting_boost_modal,
delete_modal: object.current_account.user.setting_delete_modal,
auto_play_gif: object.current_account.user.setting_auto_play_gif,
system_font_ui: object.current_account.user.setting_system_font_ui,
}
if object.current_account
store[:me] = object.current_account.id
store[:boost_modal] = object.current_account.user.setting_boost_modal
store[:delete_modal] = object.current_account.user.setting_delete_modal
store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif
store[:system_font_ui] = object.current_account.user.setting_system_font_ui
end
store
end
def compose
{
me: object.current_account.id,
default_privacy: object.current_account.user.setting_default_privacy,
default_sensitive: object.current_account.user.setting_default_sensitive,
}
store = {}
if object.current_account
store[:me] = object.current_account.id
store[:default_privacy] = object.current_account.user.setting_default_privacy
store[:default_sensitive] = object.current_account.user.setting_default_sensitive
end
store
end
def accounts
store = {}
store[object.current_account.id] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer)
store[object.admin.id] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) unless object.admin.nil?
store[object.current_account.id] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account
store[object.admin.id] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin
store
end

View File

@ -0,0 +1,25 @@
.features-list
.features-list__row
.text
%h6= t 'about.features.real_conversation_title'
= t 'about.features.real_conversation_body'
.visual
= fa_icon 'fw comments'
.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.within_reach_title'
= t 'about.features.within_reach_body'
.visual
= fa_icon 'fw mobile'
.features-list__row
.text
%h6= t 'about.features.humane_approach_title'
= t 'about.features.humane_approach_body'
.visual
= fa_icon 'fw leaf'

View File

@ -1,10 +1,13 @@
= simple_form_for(new_user, url: user_registration_path) do |f|
= f.simple_fields_for :account do |account_fields|
= account_fields.input :username,
autofocus: true,
placeholder: t('simple_form.labels.defaults.username'),
required: true,
input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
.input-with-append
= account_fields.input :username,
autofocus: true,
placeholder: t('simple_form.labels.defaults.username'),
required: true,
input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
.append
= "@#{site_hostname}"
= f.input :email,
placeholder: t('simple_form.labels.defaults.email'),
@ -22,9 +25,6 @@
input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') }
.actions
= f.button :button, t('about.get_started'), type: :submit
= f.button :button, t('auth.register'), type: :submit, class: 'button button-alternative'
.info
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
·
= link_to t('about.about_this'), about_more_path
%p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path)

View File

@ -1,4 +1,5 @@
- content_for :header_tags do
%script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
= javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
- content_for :page_title do
@ -9,79 +10,70 @@
%meta{ property: 'og:url', content: about_url }/
%meta{ property: 'og:type', content: 'website' }/
%meta{ property: 'og:title', content: site_hostname }/
%meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon')) }/
%meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon_html')) }/
%meta{ property: 'og:image', content: asset_pack_path('mastodon_small.jpg', protocol: :request) }/
%meta{ property: 'og:image:width', content: '400' }/
%meta{ property: 'og:image:height', content: '400' }/
%meta{ property: 'twitter:card', content: 'summary' }/
.wrapper
%h1
= image_tag asset_pack_path('logo.png')
= Setting.site_title
.landing-page
.header-wrapper
.mascot-container
= image_tag asset_pack_path('elephant-fren.png'), class: 'mascot'
%p!= t('about.about_mastodon')
.header
.container.links
.brand
= link_to root_url do
= image_tag asset_pack_path('logo.svg')
Mastodon
.screenshot-with-signup
.mascot= image_tag asset_pack_path('fluffy-elephant-friend.png')
%ul.nav
%li
- if user_signed_in?
= link_to t('settings.back'), root_url, class: 'webapp-btn'
- else
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
%li= link_to t('about.about_this'), about_more_path
%li= link_to t('about.other_instances'), 'https://joinmastodon.org/'
- if @instance_presenter.open_registrations
= render 'registration'
- else
.closed-registrations-message
- if @instance_presenter.closed_registrations_message.blank?
%p= t('about.closed_registrations')
.container.hero
.floats
= image_tag asset_pack_path('cloud2.png'), class: 'float-1'
= image_tag asset_pack_path('cloud3.png'), class: 'float-2'
= image_tag asset_pack_path('cloud4.png'), class: 'float-3'
.heading
%h1
= @instance_presenter.site_title
%small= t 'about.hosted_on', domain: site_hostname
- if @instance_presenter.open_registrations
= render 'registration'
- else
!= @instance_presenter.closed_registrations_message
.info
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
·
= link_to t('about.other_instances'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md'
·
= link_to t('about.about_this'), about_more_path
.closed-registrations-message
%div
- if @instance_presenter.closed_registrations_message.blank?
%p= t('about.closed_registrations')
- else
= @instance_presenter.closed_registrations_message.html_safe
= link_to t('about.find_another_instance'), 'https://joinmastodon.org', class: 'button button-alternative button--block'
%h3= t('about.features_headline')
.learn-more-cta
.container
%h3= t('about.description_headline', domain: site_hostname)
%p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
.features-list
.features-list__column
%ul.fa-ul
%li
= fa_icon('li check-square')
= t 'about.features.chronology'
%li
= fa_icon('li check-square')
= t 'about.features.public'
%li
= fa_icon('li check-square')
= t 'about.features.characters'
%li
= fa_icon('li check-square')
= t 'about.features.gifv'
.features-list__column
%ul.fa-ul
%li
= fa_icon('li check-square')
= t 'about.features.privacy'
%li
= fa_icon('li check-square')
= t 'about.features.blocks'
%li
= fa_icon('li check-square')
= t 'about.features.ethics'
%li
= fa_icon('li check-square')
= t 'about.features.api'
.features
.container
- if Setting.timeline_preview
#mastodon-timeline{ data: { props: Oj.dump(default_props) } }
- unless @instance_presenter.site_description.blank?
%h3= t('about.description_headline', domain: site_hostname)
%p!= @instance_presenter.site_description
.actions
.info
= link_to t('about.terms'), terms_path
·
= link_to t('about.apps'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md'
·
= link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
·
= link_to t('about.other_instances'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md'
.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'
= render 'features'
.footer-links
.container
%p
= link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
= " (#{@instance_presenter.version_number})"

View File

@ -12,54 +12,53 @@
%tr
%td
%strong= t('admin.settings.contact_information.label')
%td= text_field_tag :site_contact_username,
@settings['site_contact_username'].value,
place_holder: t('admin.settings.contact_information.username')
%td= text_field_tag :site_contact_username, @settings['site_contact_username'].value, place_holder: t('admin.settings.contact_information.username')
%tr
%td
%strong= t('admin.accounts.email')
%td= text_field_tag :site_contact_email,
@settings['site_contact_email'].value,
place_holder: t('admin.settings.contact_information.email')
%td= text_field_tag :site_contact_email, @settings['site_contact_email'].value, place_holder: t('admin.settings.contact_information.email')
%tr
%td
%strong= t('admin.settings.site_title')
%td= text_field_tag :site_title,
@settings['site_title'].value
%td= text_field_tag :site_title, @settings['site_title'].value
%tr
%td
%strong= t('admin.settings.site_description.title')
%p= t('admin.settings.site_description.desc_html')
%td= text_area_tag :site_description,
@settings['site_description'].value,
rows: 8
%td= text_area_tag :site_description, @settings['site_description'].value, rows: 8
%tr
%td
%strong= t('admin.settings.site_description_extended.title')
%p= t('admin.settings.site_description_extended.desc_html')
%td= text_area_tag :site_extended_description,
@settings['site_extended_description'].value,
rows: 8
%td= text_area_tag :site_extended_description, @settings['site_extended_description'].value, rows: 8
%tr
%td
%strong= t('admin.settings.site_terms.title')
%p= t('admin.settings.site_terms.desc_html')
%td= text_area_tag :site_terms,
@settings['site_terms'].value,
rows: 8
%td= text_area_tag :site_terms, @settings['site_terms'].value, rows: 8
%tr
%td
%strong= t('admin.settings.registrations.open.title')
%p= t('admin.settings.registrations.open.desc_html')
%td
= select_tag :open_registrations,
options_for_select({ t('admin.settings.registrations.open.disabled') => false, t('admin.settings.registrations.open.enabled') => true }, @settings['open_registrations'].value)
= select_tag :open_registrations, options_for_select({ t('simple_form.no') => false, t('simple_form.yes') => true }, @settings['open_registrations'].value)
%tr
%td
%strong= t('admin.settings.registrations.closed_message.title')
%p= t('admin.settings.registrations.closed_message.desc_html')
%td= text_area_tag :closed_registrations_message,
@settings['closed_registrations_message'].value,
rows: 8
%td= text_area_tag :closed_registrations_message, @settings['closed_registrations_message'].value, rows: 8
%tr
%td
%strong= t('admin.settings.registrations.deletion.title')
%p= t('admin.settings.registrations.deletion.desc_html')
%td
= select_tag :open_deletion, options_for_select({ t('simple_form.no') => false, t('simple_form.yes') => true }, @settings['open_deletion'].value)
%tr
%td
%strong= t('admin.settings.timeline_preview.title')
%p= t('admin.settings.timeline_preview.desc_html')
%td
= select_tag :timeline_preview, options_for_select({ t('simple_form.no') => false, t('simple_form.yes') => true }, @settings['timeline_preview'].value)
.simple_form.actions
= button_tag t('generic.save_changes'), type: :submit, class: :btn

View File

@ -5,7 +5,10 @@
= render 'shared/error_messages', object: resource
= f.simple_fields_for :account do |ff|
= ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
.input-with-append
= ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
.append
= "@#{site_hostname}"
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
= f.input :password, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
@ -14,4 +17,5 @@
.actions
= f.button :button, t('auth.register'), type: :submit
%p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path)
.form-footer= render 'auth/shared/links'

View File

@ -6,7 +6,8 @@
.logo-container
%h1
= link_to root_path do
= image_tag asset_pack_path('logo.png')
= image_tag asset_pack_path('logo.svg')
Mastodon
.form-container
= render 'flashes'