Compare commits

...

26 Commits

Author SHA1 Message Date
kibigo!
4d0fde3d93 Timeline should be a feature, not component 2017-06-24 17:10:55 -07:00
kibigo!
ff0ecca84c Removing vestigial hasUnread, pt. II 2017-06-24 16:37:02 -07:00
kibigo!
cdd7f792cb Removed vestigial hasUnread 2017-06-24 16:15:43 -07:00
kibigo!
77908849bb Unify timeline code 2017-06-24 15:55:55 -07:00
beatrix-bitrot
8b23bf7cbd clean up old avatar class 2017-06-24 03:51:01 +00:00
Matthew Walsh
f1a60d4b81 Unified avatar styling
Avatars now have consistent styling across all pages; border radius can be adjusted with a SASS variable ($ui-avatar-border-size)
2017-06-24 03:03:27 +00:00
kibigo!
2513d92c54 Un-hide dropdown menu ;P 2017-06-23 19:39:44 -07:00
kibigo!
414dfb3955 ESLint improvements for Profile Metadata 2017-06-23 18:43:30 -07:00
Gô Shoemake
67adbcc60c Reblog support for collapsed toots 2017-06-23 18:23:26 -07:00
beatrix-bitrot
453b9c6e7e missing punctuation 2017-06-23 22:01:04 +00:00
beatrix
d9b9bb8c5e glitch the getting started image 2017-06-23 21:50:45 +00:00
kibigo!
40ecbfd4a9 Very minor styling improvements to toot-collapsing 2017-06-23 21:50:45 +00:00
kibigo!
4fe45dda9a Updates height upon collapsing 2017-06-23 21:50:45 +00:00
kibigo!
4bd7482a7a Minor collapsing button improvements~ 2017-06-23 21:50:45 +00:00
kibigo!
93c52301ad Collapsable toots [1/??] 2017-06-23 21:50:45 +00:00
kibigo!
0d3ec19e89 Profile Metadata HACK 😈 2017-06-23 21:45:14 +00:00
Go Shoemake
62a75891ab Fixes drawer so stuff doesn't overflow 2017-06-23 21:45:14 +00:00
Charlotte Fields
b27842dc70 cybre cleanup 2017-06-23 21:45:14 +00:00
Chronister
39b6b37b74 cybrespace to 1.4.2 2017-06-23 21:45:14 +00:00
Chronister
65528fc54e All cybrespace changes through 5/28 2017-06-23 21:45:14 +00:00
Charlotte Fields
382572c213 adding cybre changes 2017-06-23 21:45:14 +00:00
beatrix-bitrot
9bc593d675 update local modifications for cors and cp 2017-06-23 21:45:14 +00:00
beatrix-bitrot
09f7ad3614 silly readme update to test automated deploys 2017-06-23 21:45:14 +00:00
beatrix-bitrot
7c2ea42cd5 update README.md 2017-06-23 21:45:14 +00:00
beatrix
ea785d0baf Update README.md 2017-06-23 21:45:14 +00:00
Beatrix Bitrot
a337c5dbe5 CORS tweaks 2017-06-23 21:45:14 +00:00
33 changed files with 676 additions and 590 deletions

View File

@@ -1,70 +1,5 @@
Mastodon
Mastodon Glitch Edition
========
Now with automated deploys!
[![Build Status](http://img.shields.io/travis/tootsuite/mastodon.svg)][travis]
[![Code Climate](https://img.shields.io/codeclimate/github/tootsuite/mastodon.svg)][code_climate]
[travis]: https://travis-ci.org/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
Mastodon is a free, open-source social network server. A decentralized solution to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.
An alternative implementation of the GNU social project. Based on [ActivityStreams](https://en.wikipedia.org/wiki/Activity_Streams_(format)), [Webfinger](https://en.wikipedia.org/wiki/WebFinger), [PubsubHubbub](https://en.wikipedia.org/wiki/PubSubHubbub) and [Salmon](https://en.wikipedia.org/wiki/Salmon_(protocol)).
Click on the screenshot to watch a demo of the UI:
[![Screenshot](https://i.imgur.com/pG3Nnz3.jpg)][youtube_demo]
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
The project focus is a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd`
[patreon]: https://www.patreon.com/user?u=619786
## Resources
- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)
- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
- [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
## Features
- **Fully interoperable with GNU social and any OStatus platform**
Whatever implements Atom feeds, ActivityStreams, Salmon, PubSubHubbub and Webfinger is part of the network
- **Real-time timeline updates**
See the updates of people you're following appear in real-time in the UI via WebSockets
- **Federated thread resolving**
If someone you follow replies to a user unknown to the server, the server fetches the full thread so you can view it without leaving the UI
- **Media attachments like images and WebM**
Upload and view images and WebM videos attached to the updates
- **OAuth2 and a straightforward REST API**
Mastodon acts as an OAuth2 provider so 3rd party apps can use the API, which is RESTful and simple
- **Background processing for long-running tasks**
Mastodon tries to be as fast and responsive as possible, so all long-running tasks that can be delegated to background processing, are
- **Deployable via Docker**
You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
## Development
Please follow the [development guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md) from the documentation repository.
## Deployment
There are guides in the documentation repository for [deploying on various platforms](https://github.com/tootsuite/documentation#running-mastodon).
## Contributing
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository. [Here are the guidelines for code contributions](CONTRIBUTING.md)
**IRC channel**: #mastodon on irc.freenode.net
## Extra credits
- The [Emoji One](https://github.com/Ranks/emojione) pack has been used for the emojis
- The error page image courtesy of [Dopatwo](https://www.youtube.com/user/dopatwo)
![Mastodon error image](https://mastodon.social/oops.png)
So here's the deal: we all work on this code, and then it runs on dev.glitch.social and anyone who uses that does so absolutely at their own risk. can you dig it?

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -17,6 +17,7 @@ export default class IconButton extends React.PureComponent {
disabled: PropTypes.bool,
inverted: PropTypes.bool,
animate: PropTypes.bool,
flip: PropTypes.bool,
overlay: PropTypes.bool,
};
@@ -69,7 +70,7 @@ export default class IconButton extends React.PureComponent {
}
return (
<Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
<Motion defaultStyle={{ rotate: this.props.active ? (this.props.flip ? -180 : -360) : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? (this.props.flip ? -180 : -360) : 0, { stiffness: this.props.flip ? 60 : 120, damping: 7 }) : 0 }}>
{({ rotate }) =>
<button
aria-label={this.props.title}

View File

@@ -3,19 +3,24 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
import AvatarOverlay from './avatar_overlay';
import RelativeTimestamp from './relative_timestamp';
import DisplayName from './display_name';
import MediaGallery from './media_gallery';
import VideoPlayer from './video_player';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import { FormattedMessage } from 'react-intl';
import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import emojify from '../emoji';
import escapeTextContentForBrowser from 'escape-html';
import ImmutablePureComponent from 'react-immutable-pure-component';
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
export default class Status extends ImmutablePureComponent {
const messages = defineMessages({
collapse: { id: 'status.collapse', defaultMessage: 'Collapse' },
uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' },
});
class StatusUnextended extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
@@ -37,12 +42,14 @@ export default class Status extends ImmutablePureComponent {
autoPlayGif: PropTypes.bool,
muted: PropTypes.bool,
intersectionObserverWrapper: PropTypes.object,
intl: PropTypes.object.isRequired,
};
state = {
isExpanded: false,
isIntersecting: true, // assume intersecting until told otherwise
isHidden: false, // set to true in requestIdleCallback to trigger un-render
isCollapsed: false,
}
// Avoid checking props that are functions (and whose equality will always
@@ -60,7 +67,11 @@ export default class Status extends ImmutablePureComponent {
updateOnStates = ['isExpanded']
shouldComponentUpdate (nextProps, nextState) {
if (!nextState.isIntersecting && nextState.isHidden) {
if (nextState.isCollapsed !== this.state.isCollapsed) {
// If the collapsed state of the element has changed then we definitely
// need to re-update.
return true;
} else if (!nextState.isIntersecting && nextState.isHidden) {
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
// that either "isIntersecting" or "isHidden" matter, and then they're
// the only things that matter.
@@ -74,7 +85,13 @@ export default class Status extends ImmutablePureComponent {
return super.shouldComponentUpdate(nextProps, nextState);
}
componentDidUpdate (prevProps, prevState) {
if (prevState.isCollapsed !== this.state.isCollapsed) this.saveHeight();
}
componentDidMount () {
const node = this.node;
if (!this.props.intersectionObserverWrapper) {
// TODO: enable IntersectionObserver optimization for notification statuses.
// These are managed in notifications/index.js rather than status_list.js
@@ -86,6 +103,8 @@ export default class Status extends ImmutablePureComponent {
this.handleIntersection
);
if (node.clientHeight > 400 && !(this.props.status.get('reblog', null) !== null && typeof this.props.status.get('reblog') === 'object')) this.setState({ isCollapsed: true });
this.componentMounted = true;
}
@@ -150,14 +169,18 @@ export default class Status extends ImmutablePureComponent {
this.setState({ isExpanded: !this.state.isExpanded });
};
handleCollapsedClick = () => {
this.setState({ isCollapsed: !this.state.isCollapsed });
}
render () {
let media = null;
let statusAvatar;
// Exclude intersectionObserverWrapper from `other` variable
// because intersection is managed in here.
const { status, account, intersectionObserverWrapper, ...other } = this.props;
const { isExpanded, isIntersecting, isHidden } = this.state;
const { status, account, intersectionObserverWrapper, intl, ...other } = this.props;
const { isExpanded, isIntersecting, isHidden, isCollapsed } = this.state;
if (status === null) {
return null;
@@ -210,9 +233,17 @@ export default class Status extends ImmutablePureComponent {
}
return (
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} ref={this.handleRef}>
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')} ${isCollapsed ? 'status-collapsed' : ''}`} data-id={status.get('id')} ref={this.handleRef}>
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<IconButton
className='status__collapse-button'
animate flip
active={isCollapsed}
title={isCollapsed ? intl.formatMessage(messages.uncollapse) : intl.formatMessage(messages.collapse)}
icon='angle-double-up'
onClick={this.handleCollapsedClick}
/>
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'>
@@ -221,15 +252,19 @@ export default class Status extends ImmutablePureComponent {
<DisplayName account={status.get('account')} />
</a>
</div>
<StatusContent status={status} onClick={this.handleClick} expanded={isExpanded} onExpandedToggle={this.handleExpandedToggle} onHeightUpdate={this.saveHeight} />
{media}
{isCollapsed ? null : media}
<StatusActionBar {...this.props} />
{isCollapsed ? null : <StatusActionBar status={status} account={account} {...other} />}
</div>
);
}
}
const Status = injectIntl(StatusUnextended);
export default Status;

View File

@@ -5,6 +5,7 @@ import IconButton from './icon_button';
import DropdownMenu from './dropdown_menu';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import RelativeTimestamp from './relative_timestamp';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -145,6 +146,8 @@ export default class StatusActionBar extends ImmutablePureComponent {
<div className='status__action-bar-dropdown'>
<DropdownMenu items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
</div>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
</div>
);
}

View File

@@ -5,9 +5,7 @@ import emojify from '../../../emoji';
import escapeTextContentForBrowser from 'escape-html';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Motion from 'react-motion/lib/Motion';
import spring from 'react-motion/lib/spring';
import { connect } from 'react-redux';
import Avatar from '../../../components/avatar';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
@@ -16,61 +14,57 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
});
const makeMapStateToProps = () => {
const mapStateToProps = state => ({
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
});
/*
THIS IS A MESS BECAUSE EFFING MASTODON AND ITS EFFING HTML BIOS
INSTEAD OF JUST STORING EVERYTHING IN PLAIN EFFING TEXT ! ! ! !
BLANK LINES ALSO WON'T WORK BECAUSE RIGHT NOW MASTODON CONVERTS
THOSE INTO `<P>` ELEMENTS INSTEAD OF LEAVING IT AS `<BR><BR>` !
TL:DR; THIS IS LARGELY A HACK. WITH BETTER BACKEND STUFF WE CAN
IMPROVE THIS BY BETTER PREDICTING HOW THE METADATA WILL BE SENT
WHILE MAINTAINING BASIC PLAIN-TEXT PROCESSING. THE OTHER OPTION
IS TO TURN ALL BIOS INTO PLAIN-TEXT VIA A TREE-WALKER, AND THEN
PROCESS THE YAML AND LINKS AND EVERYTHING OURSELVES. THIS WOULD
BE INCREDIBLY COMPLICATED, AND IT WOULD BE A MILLION TIMES LESS
DIFFICULT IF MASTODON JUST GAVE US PLAIN-TEXT BIOS (WHICH QUITE
FRANKLY MAKES THE MOST SENSE SINCE THAT'S WHAT USERS PROVIDE IN
SETTINGS) TO BEGIN WITH AND LEFT ALL PROCESSING TO THE FRONTEND
TO HANDLE ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
ANYWAY I KNOW WHAT NEEDS TO BE DONE REGARDING BACKEND STUFF BUT
I'M NOT SMART ENOUGH TO FIGURE OUT HOW TO ACTUALLY IMPLEMENT IT
SO FEEL FREE TO @ ME IF YOU NEED MY IDEAS REGARDING THAT. UNTIL
THEN WE'LL JUST HAVE TO MAKE DO WITH THIS MESSY AND UNFORTUNATE
HACKING ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
return mapStateToProps;
with love,
@kibi@glitch.social <3
*/
const NEW_LINE = /(?:^|\r?\n|<br\s*\/?>)/g;
const YAML_OPENER = /---/;
const YAML_CLOSER = /(?:---|\.\.\.)/;
const YAML_STRING = /(?:"(?:[^"\n]){1,32}"|'(?:[^'\n]){1,32}'|(?:[^'":\n]){1,32})/g;
const YAML_LINE = new RegExp('\\s*' + YAML_STRING.source + '\\s*:\\s*' + YAML_STRING.source + '\\s*', 'g');
const BIO_REGEX = new RegExp(NEW_LINE.source + '*' + YAML_OPENER.source + NEW_LINE.source + '+(?:' + YAML_LINE.source + NEW_LINE.source + '+){0,4}' + YAML_CLOSER.source + NEW_LINE.source + '*');
const processBio = (data) => {
let props = { text: data, metadata: [] };
let yaml = data.match(BIO_REGEX);
if (!yaml) return props;
else yaml = yaml[0];
let start = props.text.indexOf(yaml);
let end = start + yaml.length;
props.text = props.text.substr(0, start) + props.text.substr(end);
yaml = yaml.replace(NEW_LINE, '\n');
let metadata = (yaml ? yaml.match(YAML_LINE) : []) || [];
for (let i = 0; i < metadata.length; i++) {
let result = metadata[i].match(YAML_STRING);
if (result[0][0] === '"' || result[0][0] === '\'') result[0] = result[0].substr(1, result[0].length - 2);
if (result[1][0] === '"' || result[1][0] === '\'') result[0] = result[1].substr(1, result[1].length - 2);
props.metadata.push(result);
}
return props;
};
class Avatar extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
autoPlayGif: PropTypes.bool.isRequired,
};
state = {
isHovered: false,
};
handleMouseOver = () => {
if (this.state.isHovered) return;
this.setState({ isHovered: true });
}
handleMouseOut = () => {
if (!this.state.isHovered) return;
this.setState({ isHovered: false });
}
render () {
const { account, autoPlayGif } = this.props;
const { isHovered } = this.state;
return (
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
{({ radius }) =>
<a // eslint-disable-line jsx-a11y/anchor-has-content
href={account.get('url')}
className='account__header__avatar'
target='_blank'
rel='noopener'
style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
onFocus={this.handleMouseOver}
onBlur={this.handleMouseOut}
/>
}
</Motion>
);
}
}
@connect(makeMapStateToProps)
@injectIntl
export default class Header extends ImmutablePureComponent {
@@ -79,7 +73,6 @@ export default class Header extends ImmutablePureComponent {
me: PropTypes.number.isRequired,
onFollow: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
autoPlayGif: PropTypes.bool.isRequired,
};
render () {
@@ -122,21 +115,45 @@ export default class Header extends ImmutablePureComponent {
lockedIcon = <i className='fa fa-lock' />;
}
const content = { __html: emojify(account.get('note')) };
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
const { text, metadata } = processBio(account.get('note'));
return (
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
<div>
<Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
<div className='account__header__wrapper'>
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
<div>
<a href={account.get('url')} target='_blank' rel='noopener'>
<span className='account__header__avatar'><Avatar src={account.get('avatar')} animate size={90} /></span>
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
</a>
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} />
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
<div className='account__header__content' dangerouslySetInnerHTML={content} />
{info}
{actionBtn}
{info}
{actionBtn}
</div>
</div>
{metadata.length && (
<div className='account__metadata'>
{(() => {
let data = [];
for (let i = 0; i < metadata.length; i++) {
data.push(
<div
className='account__metadata-item'
title={metadata[i][0] + ':' + metadata[i][1]}
key={i}
>
<span dangerouslySetInnerHTML={{ __html: emojify(metadata[i][0]) }} />
<strong dangerouslySetInnerHTML={{ __html: emojify(metadata[i][1]) }} />
</div>
);
}
return data;
})()}
</div>
) || null}
</div>
);
}

View File

@@ -1,144 +1,44 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import {
refreshCommunityTimeline,
expandCommunityTimeline,
updateTimeline,
deleteFromTimelines,
connectTimeline,
disconnectTimeline,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
import createStream from '../../stream';
import Timeline from '../timeline';
const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' },
});
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
});
@connect(mapStateToProps)
@injectIntl
export default class CommunityTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('COMMUNITY', {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
componentDidMount () {
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
dispatch(refreshCommunityTimeline());
if (typeof this._subscription !== 'undefined') {
return;
}
this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
connected () {
dispatch(connectTimeline('community'));
},
reconnected () {
dispatch(connectTimeline('community'));
},
disconnected () {
dispatch(disconnectTimeline('community'));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('community', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
}
componentWillUnmount () {
if (typeof this._subscription !== 'undefined') {
this._subscription.close();
this._subscription = null;
}
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(expandCommunityTimeline());
}
render () {
const { intl, hasUnread, columnId, multiColumn } = this.props;
const pinned = !!columnId;
const { intl, columnId, multiColumn } = this.props;
return (
<Column ref={this.setRef}>
<ColumnHeader
icon='users'
active={hasUnread}
title={intl.formatMessage(messages.title)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
>
<ColumnSettingsContainer />
</ColumnHeader>
<StatusListContainer
trackScroll={!pinned}
scrollKey={`community_timeline-${columnId}`}
timelineId='community'
loadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
/>
</Column>
<Timeline
expand={expandCommunityTimeline}
refresh={refreshCommunityTimeline}
streamId='public:local'
columnName='COMMUNITY'
columnId={columnId}
mulitColumn={multiColumn}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
icon='users'
title={intl.formatMessage(messages.title)}
settings={<ColumnSettingsContainer />}
scrollName='community_timeline'
timelineId='community'
/>
);
}

View File

@@ -1,138 +1,53 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import {
refreshHashtagTimeline,
expandHashtagTimeline,
updateTimeline,
deleteFromTimelines,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { FormattedMessage } from 'react-intl';
import createStream from '../../stream';
import Timeline from '../timeline';
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
});
@connect(mapStateToProps)
export default class HashtagTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
columnId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('HASHTAG', { id: this.props.params.id }));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
_subscribe (dispatch, id) {
const { streamingAPIBaseURL, accessToken } = this.props;
this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, {
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(`hashtag:${id}`, JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
}
_unsubscribe () {
if (typeof this.subscription !== 'undefined') {
this.subscription.close();
this.subscription = null;
}
}
componentDidMount () {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(refreshHashtagTimeline(id));
this._subscribe(dispatch, id);
componentWillMount () {
const id = this.props.params.id;
this.expand = () => expandHashtagTimeline(id);
this.refresh = () => refreshHashtagTimeline(id);
}
componentWillReceiveProps (nextProps) {
if (nextProps.params.id !== this.props.params.id) {
this.props.dispatch(refreshHashtagTimeline(nextProps.params.id));
this._unsubscribe();
this._subscribe(this.props.dispatch, nextProps.params.id);
const id = nextProps.params.id;
this.expand = () => expandHashtagTimeline(id);
this.refresh = () => refreshHashtagTimeline(id);
}
}
componentWillUnmount () {
this._unsubscribe();
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(expandHashtagTimeline(this.props.params.id));
}
render () {
const { hasUnread, columnId, multiColumn } = this.props;
const { columnId, multiColumn } = this.props;
const { id } = this.props.params;
const pinned = !!columnId;
return (
<Column ref={this.setRef}>
<ColumnHeader
icon='hashtag'
active={hasUnread}
title={id}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
showBackButton
/>
<StatusListContainer
trackScroll={!pinned}
scrollKey={`hashtag_timeline-${columnId}`}
timelineId={`hashtag:${id}`}
loadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
/>
</Column>
<Timeline
expand={this.expand}
refresh={this.refresh}
streamId={`hashtag&tag=${id}`}
columnName='HASHTAG'
columnProps={{ id }}
columnId={columnId}
mulitColumn={multiColumn}
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
icon='hashtag'
title={id}
scrollName='hashtag_timeline'
timelineId={`hashtag:${id}`}
/>
);
}

View File

@@ -2,12 +2,9 @@ import React from 'react';
import { connect } from 'react-redux';
import { expandHomeTimeline } from '../../actions/timelines';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
import Timeline from '../timeline';
import Link from 'react-router-dom/Link';
const messages = defineMessages({
@@ -15,7 +12,6 @@ const messages = defineMessages({
});
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
hasFollows: state.getIn(['accounts_counters', state.getIn(['meta', 'me']), 'following_count']) > 0,
});
@@ -24,44 +20,14 @@ const mapStateToProps = state => ({
export default class HomeTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
hasFollows: PropTypes.bool,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
};
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('HOME', {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(expandHomeTimeline());
}
render () {
const { intl, hasUnread, hasFollows, columnId, multiColumn } = this.props;
const pinned = !!columnId;
const { intl, hasFollows, columnId, multiColumn } = this.props;
let emptyMessage;
@@ -72,28 +38,18 @@ export default class HomeTimeline extends React.PureComponent {
}
return (
<Column ref={this.setRef}>
<ColumnHeader
icon='home'
active={hasUnread}
title={intl.formatMessage(messages.title)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
>
<ColumnSettingsContainer />
</ColumnHeader>
<StatusListContainer
trackScroll={!pinned}
scrollKey={`home_timeline-${columnId}`}
loadMore={this.handleLoadMore}
timelineId='home'
emptyMessage={emptyMessage}
/>
</Column>
<Timeline
expand={expandHomeTimeline}
columnName='HOME'
columnId={columnId}
mulitColumn={multiColumn}
emptyMessage={emptyMessage}
icon='home'
title={intl.formatMessage(messages.title)}
settings={<ColumnSettingsContainer />}
scrollName='home_timeline'
timelineId='home'
/>
);
}

View File

@@ -1,144 +1,44 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import {
refreshPublicTimeline,
expandPublicTimeline,
updateTimeline,
deleteFromTimelines,
connectTimeline,
disconnectTimeline,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
import createStream from '../../stream';
import Timeline from '../timeline';
const messages = defineMessages({
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
});
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
});
@connect(mapStateToProps)
@injectIntl
export default class PublicTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool,
};
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('PUBLIC', {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
componentDidMount () {
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
dispatch(refreshPublicTimeline());
if (typeof this._subscription !== 'undefined') {
return;
}
this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
connected () {
dispatch(connectTimeline('public'));
},
reconnected () {
dispatch(connectTimeline('public'));
},
disconnected () {
dispatch(disconnectTimeline('public'));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('public', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
}
componentWillUnmount () {
if (typeof this._subscription !== 'undefined') {
this._subscription.close();
this._subscription = null;
}
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(expandPublicTimeline());
}
render () {
const { intl, columnId, hasUnread, multiColumn } = this.props;
const pinned = !!columnId;
const { intl, columnId, multiColumn } = this.props;
return (
<Column ref={this.setRef}>
<ColumnHeader
icon='globe'
active={hasUnread}
title={intl.formatMessage(messages.title)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
>
<ColumnSettingsContainer />
</ColumnHeader>
<StatusListContainer
timelineId='public'
loadMore={this.handleLoadMore}
trackScroll={!pinned}
scrollKey={`public_timeline-${columnId}`}
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />}
/>
</Column>
<Timeline
expand={expandPublicTimeline}
refresh={refreshPublicTimeline}
streamId='public'
columnName='PUBLIC'
columnId={columnId}
mulitColumn={multiColumn}
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />}
icon='globe'
title={intl.formatMessage(messages.title)}
settings={<ColumnSettingsContainer />}
scrollName='public_timeline'
timelineId='public'
/>
);
}

View File

@@ -0,0 +1,179 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import {
updateTimeline,
deleteFromTimelines,
connectTimeline,
disconnectTimeline,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import createStream from '../../stream';
const mapStateToProps = (state, ownprops) => ({
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
hasUnread: state.getIn(['timelines', ownprops.timelineId, 'unread']) > 0,
});
@connect(mapStateToProps)
export default class Timeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
expand: PropTypes.func.isRequired,
refresh: PropTypes.func,
streamId: PropTypes.string,
hasUnread: PropTypes.bool,
columnName: PropTypes.string.isRequired,
columnProps: PropTypes.object,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
emptyMessage: PropTypes.oneOfType([
PropTypes.element,
PropTypes.string,
]),
icon: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
settings: PropTypes.element,
scrollName: PropTypes.string.isRequired,
timelineId: PropTypes.string.isRequired,
};
handlePin = () => {
const { columnName, columnProps, columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn(columnName, columnProps || {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(this.props.expand());
}
_subscribe (dispatch, streamId, timelineId) {
const { streamingAPIBaseURL, accessToken } = this.props;
if (!streamId || !timelineId) return;
this.subscription = createStream(streamingAPIBaseURL, accessToken, streamId, {
connected () {
dispatch(connectTimeline(timelineId));
},
reconnected () {
dispatch(connectTimeline(timelineId));
},
disconnected () {
dispatch(disconnectTimeline(timelineId));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
}
_unsubscribe () {
if (typeof this.subscription !== 'undefined') {
this.subscription.close();
this.subscription = null;
}
}
componentDidMount () {
const { dispatch, refresh, streamId, timelineId } = this.props;
if (typeof refresh !== 'function') return;
dispatch(refresh());
this._subscribe(dispatch, streamId, timelineId);
}
componentWillReceiveProps (nextProps) {
if (nextProps.streamId !== this.props.streamId || nextProps.timelineId !== this.props.timelineId) {
if (typeof refresh !== 'function') return;
this.props.dispatch(this.props.refresh());
this._unsubscribe();
this._subscribe(this.props.dispatch, nextProps.streamId, nextProps.timelineId);
}
}
componentWillUnmount () {
this._unsubscribe();
}
render () {
const {
hasUnread,
columnId,
multiColumn,
emptyMessage,
icon,
title,
settings,
scrollName,
timelineId,
} = this.props;
const pinned = !!columnId;
return (
<Column ref={this.setRef}>
<ColumnHeader
icon={icon}
active={hasUnread}
title={title}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
>
{settings}
</ColumnHeader>
<StatusListContainer
trackScroll={!pinned}
scrollKey={`${scrollName}-${columnId}`}
loadMore={this.handleLoadMore}
timelineId={timelineId}
emptyMessage={emptyMessage}
/>
</Column>
);
}
}

View File

@@ -191,6 +191,14 @@
{
"defaultMessage": "{name} boosted",
"id": "status.reblogged_by"
},
{
"defaultMessage": "Collapse",
"id": "status.collapse"
},
{
"defaultMessage": "Uncollapse",
"id": "status.uncollapse"
}
],
"path": "app/javascript/mastodon/components/status.json"
@@ -1170,4 +1178,4 @@
],
"path": "app/javascript/mastodon/features/ui/components/video_modal.json"
}
]
]

View File

@@ -143,6 +143,7 @@
"search.placeholder": "Search",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"status.cannot_reblog": "This post cannot be boosted",
"status.collapse": "Collapse",
"status.delete": "Delete",
"status.favourite": "Favourite",
"status.load_more": "Load more",
@@ -159,6 +160,7 @@
"status.sensitive_warning": "Sensitive content",
"status.show_less": "Show less",
"status.show_more": "Show more",
"status.uncollapse": "Uncollapse",
"status.unmute_conversation": "Unmute conversation",
"tabs_bar.compose": "Compose",
"tabs_bar.federated_timeline": "Federated",

View File

@@ -0,0 +1 @@
require('../styles/custom.scss');

View File

@@ -1,5 +1,5 @@
@mixin avatar-radius() {
border-radius: 4px;
border-radius: $ui-avatar-border-size;
background: transparent no-repeat;
background-position: 50%;
background-clip: padding-box;

View File

@@ -172,16 +172,14 @@
text-align: center;
.avatar {
width: 80px;
height: 80px;
@include avatar-size(80px);
margin: 0 auto;
margin-bottom: 15px;
img {
@include avatar-radius();
@include avatar-size(80px);
display: block;
width: 80px;
height: 80px;
border-radius: 48px;
}
}

View File

@@ -46,17 +46,16 @@
}
.avatar {
width: 120px;
@include avatar-size(120px);
margin: 0 auto;
margin-bottom: 15px;
position: relative;
z-index: 2;
img {
width: 120px;
height: 120px;
@include avatar-radius();
@include avatar-size(120px);
display: block;
border-radius: 120px;
}
}
@@ -283,16 +282,14 @@
}
.avatar {
width: 60px;
height: 60px;
@include avatar-size(60px);
float: left;
margin-right: 15px;
img {
@include avatar-radius();
@include avatar-size(60px);
display: block;
width: 60px;
height: 60px;
border-radius: 60px;
}
}
@@ -359,15 +356,14 @@
}
& > div {
@include avatar-size(48px);
float: left;
margin-right: 10px;
width: 48px;
height: 48px;
}
.avatar {
@include avatar-radius();
display: block;
border-radius: 4px;
}
.display-name {

View File

@@ -456,9 +456,11 @@
.reply-indicator__content {
font-size: 15px;
line-height: 20px;
color: $primary-text-color;
word-wrap: break-word;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre-wrap;
.emojione {
@@ -542,6 +544,7 @@
padding: 8px 10px;
padding-left: 68px;
position: relative;
height: auto;
min-height: 48px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: default;
@@ -598,6 +601,14 @@
}
}
}
&.status-collapsed {
height: 48px;
.status__content {
height: 20px;
}
}
}
.notification-favourite {
@@ -611,8 +622,8 @@
}
.status__relative-time {
margin-left: auto;
color: lighten($ui-base-color, 26%);
float: right;
font-size: 14px;
}
@@ -627,7 +638,13 @@
}
.status__info {
margin: 2px 0 0;
font-size: 15px;
line-height: 24px;
}
.status__collapse-button {
float: right;
}
.status-check-box {
@@ -820,9 +837,12 @@
padding: 10px;
}
.account__header {
.account__header__wrapper {
flex: 0 0 auto;
background: lighten($ui-base-color, 4%);
}
.account__header {
text-align: center;
background-size: cover;
background-position: center;
@@ -887,6 +907,42 @@
}
}
.account__metadata {
display: block;
font-size: 15px;
line-height: 36px;
overflow: hidden;
.account__metadata-item {
display: flex;
flex-direction: row;
border-top: 1px solid lighten($ui-base-color, 8%);
& > span, & > strong {
display: inline-block;
padding: 10px 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
& > span {
flex: 0 0 auto;
width: 120px;
color: $ui-primary-color;
background: lighten($ui-base-color, 13%);
font-variant: small-caps;
}
& > strong {
flex: auto;
color: $primary-text-color;
background: $ui-base-color;
font-weight: bold;
}
}
}
.account__action-bar {
border-top: 1px solid lighten($ui-base-color, 8%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
@@ -948,12 +1004,11 @@
}
.account__header__avatar {
background-size: 90px 90px;
@include avatar-radius();
@include avatar-size(90px);
display: block;
height: 90px;
margin: 0 auto 10px;
overflow: hidden;
width: 90px;
}
.account-authorize {

View File

@@ -0,0 +1,123 @@
@import 'application';
@media screen and (min-width: 1300px) {
.column {
flex-grow: 1 !important;
max-width: 400px;
}
.drawer {
flex-grow: 1 !important;
flex-basis: 200px !important;
min-width: 268px;
max-width: 400px;
}
}
.muted {
.status__content p, .status__content a {
color: lighten($ui-base-color, 35%);
}
.status__display-name strong {
color: lighten($ui-base-color, 35%);
}
}
.status time:after,
.detailed-status__datetime span:after {
font: normal normal normal 14px/1 FontAwesome;
content: "\00a0\00a0\f08e";
}
.compose-form__buttons button.active:last-child {
color:$ui-secondary-color;
background-color: $ui-highlight-color;
border-radius:3px;
}
.about-body .mascot {
display:none;
}
.screenshot-with-signup {
min-height:300px;
}
.screenshot-with-signup .closed-registrations-message,
.screenshot-with-signup form {
background-color: rgba(0,0,0,0.7);
margin:auto;
}
.screenshot-with-signup .closed-registrations-message .clock {
font-size:150%;
}
.drawer .drawer__inner {
overflow: visible;
height:inherit;
}
.drawer__pager {
overflow-y:auto;
}
.column {
// trying to fix @mdhughes safari problem
max-height:100vh;
}
.media-gallery {
height:auto !important;
max-height:30vh;
position:relative;
margin-top:20px;
margin-left:-68px;
width: calc(100% + 80px);
}
.media-gallery:before{
content: "";
display: block;
padding-top: 100%;
}
.media-gallery__item,
.media-gallery .media-spoiler{
left: 0;
right: 0;
top: 0;
bottom: 0 !important;
position:absolute;
}
.media-spoiler-video:before {
content:"";
display:block;
padding-top:100%;
}
.media-spoiler-video,
.status__video-player,
.detailed-status > .media-spoiler,
.status > .media-spoiler {
height:auto !important;
max-height:30vh;
position:relative;
margin-top:20px;
margin-left:-68px;
width: calc(100% + 80px) !important;
}
.status__video-player-video {
transform:unset;
}
.detailed-status > .media-spoiler,
.status > .media-spoiler {
height:30vh !important;
vertical-align:middle;
}

View File

@@ -64,19 +64,17 @@
.status__avatar {
position: absolute;
@include avatar-size(48px);
left: 14px;
top: 14px;
width: 48px;
height: 48px;
& > div {
width: 48px;
height: 48px;
@include avatar-size(48px);
}
img {
@include avatar-radius();
display: block;
border-radius: 4px;
}
}
@@ -164,12 +162,11 @@
}
.avatar {
width: 48px;
height: 48px;
@include avatar-size(48px);
img {
@include avatar-radius();
display: block;
border-radius: 4px;
}
}

View File

@@ -26,3 +26,6 @@ $ui-base-color: $classic-base-color !default; // Darkest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default; // Vibrant
// Avatar border size (8% default, 100% for rounded avatars)
$ui-avatar-border-size: 8%;

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
class StatusLengthValidator < ActiveModel::Validator
MAX_CHARS = 500
MAX_CHARS = 512
def validate(status)
return unless status.local? && !status.reblog?

View File

@@ -9,4 +9,4 @@
%li= link_to t('about.get_started'), new_user_registration_path
%li= link_to t('auth.login'), new_user_session_path
%li= link_to t('about.terms'), terms_path
%li= link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
%li= link_to t('about.source_code'), 'https://github.com/chronister/mastodon'

View File

@@ -36,7 +36,7 @@
.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.other_instances'), 'https://instances.mastodon.xyz/'
·
= link_to t('about.about_this'), about_more_path
@@ -82,6 +82,6 @@
·
= 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.source_code'), 'https://github.com/chronister/mastodon'
·
= link_to t('about.other_instances'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md'
= link_to t('about.other_instances'), 'https://instances.mastodon.xyz/'

View File

@@ -67,12 +67,17 @@ module Mastodon
config.active_job.queue_adapter = :sidekiq
#config.middleware.insert_before 0, Rack::Cors, debug: true, logger: (-> { Rails.logger }) do
config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '/@:username', headers: :any, methods: [:get], credentials: false
resource '/api/*', headers: :any, methods: [:post, :put, :delete, :get, :patch, :options], credentials: false, expose: ['Link', 'X-RateLimit-Reset', 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-Request-Id']
resource '/oauth/token', headers: :any, methods: [:post], credentials: false
resource '/assets/*', headers: :any, methods: [:get, :head, :options]
resource '/stylesheets/*', headers: :any, methods: [:get, :head, :options]
resource '/javascripts/*', headers: :any, methods: [:get, :head, :options]
resource '/packs/*', headers: :any, methods: [:get, :head, :options]
end
end

View File

@@ -93,9 +93,12 @@ Rails.application.configure do
end
config.action_dispatch.default_headers = {
'Server' => 'Mastodon',
'X-Frame-Options' => 'DENY',
'X-Content-Type-Options' => 'nosniff',
'X-XSS-Protection' => '1; mode=block',
'Server' => 'Mastodon',
'X-Frame-Options' => 'DENY',
'X-Content-Type-Options' => 'nosniff',
'X-XSS-Protection' => '1; mode=block',
'Content-Security-Policy' => "frame-ancestors 'none'; object-src 'none'; script-src 'self' https://dev-static.glitch.social 'unsafe-inline'; base-uri 'none';" ,
'Referrer-Policy' => 'no-referrer, strict-origin-when-cross-origin',
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload'
}
end

View File

@@ -7,7 +7,7 @@
# For more information, see docs/Running-Mastodon/Administration-guide.md
#
defaults: &defaults
site_title: Mastodon
site_title: 'dev.glitch.social'
site_description: ''
site_extended_description: ''
site_contact_username: ''

View File

@@ -8,8 +8,8 @@
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #282c37;
color: #9baec8;
background: #181818 url("/background-photo.png");
color: #1ea21e;
text-align: center;
margin: 0;
padding: 20px;
@@ -33,7 +33,7 @@
<body>
<div class="dialog">
<img src="/oops.png" alt="Mastodon" />
<img src="/logo.png" alt="dev.glitch.social" />
<div>
<h1>We're sorry, but something went wrong.</h1>

BIN
public/background-cybre.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

View File

@@ -3,7 +3,7 @@
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#2b5797</TileColor>
<TileColor>#1ea21e</TileColor>
</tile>
</msapplication>
</browserconfig>

54
public/clock.js Normal file
View File

@@ -0,0 +1,54 @@
document.addEventListener("DOMContentLoaded", function(event) {
updateClock();
setInterval(updateClock, 1000);
});
function getNextOpen(now) {
var days = [[0, 14], [4, 18], [8, 22], [12], [2, 16], [6, 20], [10]]
var nowday = now.getUTCDay();
var nour = now.getUTCHours();
var open_hour = -1;
var d = 0;
while (open_hour == -1) {
var times = days[(nowday + d) % 7];
for (var i = 0; i < times.length; ++i) {
var time = times[i];
if (time == nour) {
return "refresh";
} else if (time > nour || d > 0) {
open_hour = time;
break;
}
}
if (open_hour == -1) {
d += 1;
nour = -1;
}
}
var open = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + d));
var ts = open.setUTCHours(open_hour);
return open;
}
function updateClock() {
var clock = document.querySelector(".closed-registrations-message .clock");
var now = new Date();
var open = getNextOpen(now);
if (open == "refresh") {
location.reload();
return;
}
var until = open - now;
var ms = until % 1000;
var s = Math.floor((until / 1000)) % 60;
var m = Math.floor((until / 1000 / 60)) % 60;
var h = Math.floor((until / 1000 / 60 / 60));
if (m < 10) m = "0" + m;
if (s < 10) s = "0" + s;
clock.innerHTML = h + ":" + m + ":" + s;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

BIN
public/riot-glitch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB