Merge branch 'messaging'

This commit is contained in:
Baptiste Lemoine 2019-12-23 17:11:04 +01:00
commit d867f9c57b
104 changed files with 30752 additions and 887 deletions

8
.gitignore vendored
View File

@ -62,3 +62,11 @@ ubuntu-xenial-16.04-cloudimg-console.log
# Ignore Docker option files # Ignore Docker option files
docker-compose.override.yml docker-compose.override.yml
/public/packs
/public/packs-test
/node_modules
/yarn-error.log
yarn-debug.log*
.yarn-integrity

1
.nope_browserlistrc Normal file
View File

@ -0,0 +1 @@
defaults

View File

@ -1 +1 @@
2.6.5 2.6.4

View File

@ -0,0 +1,2 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

View File

@ -0,0 +1,80 @@
body {
background-color: #fff;
color: #333;
margin: 33px;
}
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 13px;
line-height: 18px;
}
pre {
background-color: #eee;
padding: 10px;
font-size: 11px;
}
a {
color: #000;
}
a:visited {
color: #666;
}
a:hover {
color: #fff;
background-color: #000;
}
th {
padding-bottom: 5px;
}
td {
padding: 0 5px 7px;
}
div.field,
div.actions {
margin-bottom: 10px;
}
#notice {
color: green;
}
.field_with_errors {
padding: 2px;
background-color: red;
display: table;
}
#error_explanation {
width: 450px;
border: 2px solid red;
padding: 7px 7px 0;
margin-bottom: 20px;
background-color: #f0f0f0;
}
#error_explanation h2 {
text-align: left;
font-weight: bold;
padding: 5px 5px 5px 15px;
font-size: 12px;
margin: -7px -7px 0;
background-color: #c00;
color: #fff;
}
#error_explanation ul li {
font-size: 12px;
list-style: square;
}
label {
display: block;
}

View File

@ -0,0 +1,4 @@
/*
Place all the styles related to the matching controller here.
They will automatically be included in application.css.
*/

View File

@ -0,0 +1,58 @@
class UserGroupsController < ApplicationController
before_action :set_user_group, only: [:show, :edit, :update, :destroy]
# GET /user_groups
def index
@user_groups = UserGroup.all
end
# GET /user_groups/1
def show
end
# GET /user_groups/new
def new
@user_group = UserGroup.new
end
# GET /user_groups/1/edit
def edit
end
# POST /user_groups
def create
@user_group = UserGroup.new(user_group_params)
if @user_group.save
redirect_to @user_group, notice: 'User group was successfully created.'
else
render :new
end
end
# PATCH/PUT /user_groups/1
def update
if @user_group.update(user_group_params)
redirect_to @user_group, notice: 'User group was successfully updated.'
else
render :edit
end
end
# DELETE /user_groups/1
def destroy
@user_group.destroy
redirect_to user_groups_url, notice: 'User group was successfully destroyed.'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user_group
@user_group = UserGroup.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def user_group_params
params.require(:user_group).permit(:name, :createAt, :visibility, :members)
end
end

View File

@ -0,0 +1,2 @@
module UserGroupsHelper
end

View File

@ -1,9 +1,5 @@
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import { import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer';
importFetchedAccounts,
importFetchedStatuses,
importFetchedStatus,
} from './importer';
export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT'; export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT';
export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT'; export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT';
@ -30,7 +26,7 @@ export const unmountConversations = () => ({
export const markConversationRead = conversationId => (dispatch, getState) => { export const markConversationRead = conversationId => (dispatch, getState) => {
dispatch({ dispatch({
type: CONVERSATIONS_READ, type: CONVERSATIONS_READ,
id: conversationId, id : conversationId,
}); });
api(getState).post(`/api/v1/conversations/${conversationId}/read`); api(getState).post(`/api/v1/conversations/${conversationId}/read`);

View File

@ -3,7 +3,7 @@ import openDB from '../storage/db';
import { evictStatus } from '../storage/modifier'; import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines'; import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer'; import { importAccount, importFetchedStatus, importFetchedStatuses, importStatus } from './importer';
import { ensureComposeIsVisible } from './compose'; import { ensureComposeIsVisible } from './compose';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
@ -37,7 +37,7 @@ export function fetchStatusRequest(id, skipLoading) {
id, id,
skipLoading, skipLoading,
}; };
}; }
function getFromDB(dispatch, getState, accountIndex, index, id) { function getFromDB(dispatch, getState, accountIndex, index, id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -113,24 +113,24 @@ export function fetchStatus(id) {
dispatch(fetchStatusFail(id, error, skipLoading)); dispatch(fetchStatusFail(id, error, skipLoading));
}); });
}; };
}; }
export function fetchStatusSuccess(skipLoading) { export function fetchStatusSuccess(skipLoading) {
return { return {
type: STATUS_FETCH_SUCCESS, type: STATUS_FETCH_SUCCESS,
skipLoading, skipLoading,
}; };
}; }
export function fetchStatusFail(id, error, skipLoading) { export function fetchStatusFail(id, error, skipLoading) {
return { return {
type: STATUS_FETCH_FAIL, type : STATUS_FETCH_FAIL,
id, id,
error, error,
skipLoading, skipLoading,
skipAlert: true, skipAlert: true,
}; };
}; }
export function redraft(status, raw_text) { export function redraft(status, raw_text) {
return { return {
@ -138,7 +138,7 @@ export function redraft(status, raw_text) {
status, status,
raw_text, raw_text,
}; };
}; }
export function deleteStatus(id, routerHistory, withRedraft = false) { export function deleteStatus(id, routerHistory, withRedraft = false) {
return (dispatch, getState) => { return (dispatch, getState) => {
@ -163,29 +163,29 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
dispatch(deleteStatusFail(id, error)); dispatch(deleteStatusFail(id, error));
}); });
}; };
}; }
export function deleteStatusRequest(id) { export function deleteStatusRequest(id) {
return { return {
type: STATUS_DELETE_REQUEST, type: STATUS_DELETE_REQUEST,
id: id, id : id,
}; };
}; }
export function deleteStatusSuccess(id) { export function deleteStatusSuccess(id) {
return { return {
type: STATUS_DELETE_SUCCESS, type: STATUS_DELETE_SUCCESS,
id: id, id : id,
}; };
}; }
export function deleteStatusFail(id, error) { export function deleteStatusFail(id, error) {
return { return {
type: STATUS_DELETE_FAIL, type : STATUS_DELETE_FAIL,
id: id, id : id,
error: error, error: error,
}; };
}; }
export function fetchContext(id) { export function fetchContext(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
@ -203,33 +203,33 @@ export function fetchContext(id) {
dispatch(fetchContextFail(id, error)); dispatch(fetchContextFail(id, error));
}); });
}; };
}; }
export function fetchContextRequest(id) { export function fetchContextRequest(id) {
return { return {
type: CONTEXT_FETCH_REQUEST, type: CONTEXT_FETCH_REQUEST,
id, id,
}; };
}; }
export function fetchContextSuccess(id, ancestors, descendants) { export function fetchContextSuccess(id, ancestors, descendants) {
return { return {
type: CONTEXT_FETCH_SUCCESS, type : CONTEXT_FETCH_SUCCESS,
id, id,
ancestors, ancestors,
descendants, descendants,
statuses: ancestors.concat(descendants), statuses: ancestors.concat(descendants),
}; };
}; }
export function fetchContextFail(id, error) { export function fetchContextFail(id, error) {
return { return {
type: CONTEXT_FETCH_FAIL, type : CONTEXT_FETCH_FAIL,
id, id,
error, error,
skipAlert: true, skipAlert: true,
}; };
}; }
export function muteStatus(id) { export function muteStatus(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
@ -241,21 +241,21 @@ export function muteStatus(id) {
dispatch(muteStatusFail(id, error)); dispatch(muteStatusFail(id, error));
}); });
}; };
}; }
export function muteStatusRequest(id) { export function muteStatusRequest(id) {
return { return {
type: STATUS_MUTE_REQUEST, type: STATUS_MUTE_REQUEST,
id, id,
}; };
}; }
export function muteStatusSuccess(id) { export function muteStatusSuccess(id) {
return { return {
type: STATUS_MUTE_SUCCESS, type: STATUS_MUTE_SUCCESS,
id, id,
}; };
}; }
export function muteStatusFail(id, error) { export function muteStatusFail(id, error) {
return { return {
@ -263,7 +263,7 @@ export function muteStatusFail(id, error) {
id, id,
error, error,
}; };
}; }
export function unmuteStatus(id) { export function unmuteStatus(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
@ -275,21 +275,21 @@ export function unmuteStatus(id) {
dispatch(unmuteStatusFail(id, error)); dispatch(unmuteStatusFail(id, error));
}); });
}; };
}; }
export function unmuteStatusRequest(id) { export function unmuteStatusRequest(id) {
return { return {
type: STATUS_UNMUTE_REQUEST, type: STATUS_UNMUTE_REQUEST,
id, id,
}; };
}; }
export function unmuteStatusSuccess(id) { export function unmuteStatusSuccess(id) {
return { return {
type: STATUS_UNMUTE_SUCCESS, type: STATUS_UNMUTE_SUCCESS,
id, id,
}; };
}; }
export function unmuteStatusFail(id, error) { export function unmuteStatusFail(id, error) {
return { return {
@ -297,7 +297,7 @@ export function unmuteStatusFail(id, error) {
id, id,
error, error,
}; };
}; }
export function hideStatus(ids) { export function hideStatus(ids) {
if (!Array.isArray(ids)) { if (!Array.isArray(ids)) {
@ -308,7 +308,7 @@ export function hideStatus(ids) {
type: STATUS_HIDE, type: STATUS_HIDE,
ids, ids,
}; };
}; }
export function revealStatus(ids) { export function revealStatus(ids) {
if (!Array.isArray(ids)) { if (!Array.isArray(ids)) {
@ -319,4 +319,4 @@ export function revealStatus(ids) {
type: STATUS_REVEAL, type: STATUS_REVEAL,
ids, ids,
}; };
}; }

View File

@ -37,34 +37,37 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
export default class AutosuggestTextarea extends ImmutablePureComponent { export default class AutosuggestTextarea extends ImmutablePureComponent {
static propTypes = { static propTypes = {
value: PropTypes.string, value : PropTypes.string,
suggestions: ImmutablePropTypes.list, suggestions : ImmutablePropTypes.list,
disabled: PropTypes.bool, disabled : PropTypes.bool,
placeholder: PropTypes.string, placeholder : PropTypes.string,
onSuggestionSelected: PropTypes.func.isRequired, onSuggestionSelected : PropTypes.func.isRequired,
onSuggestionsClearRequested: PropTypes.func.isRequired, onSuggestionsClearRequested: PropTypes.func.isRequired,
onSuggestionsFetchRequested: PropTypes.func.isRequired, onSuggestionsFetchRequested: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange : PropTypes.func.isRequired,
onKeyUp: PropTypes.func, onKeyUp : PropTypes.func,
onKeyDown: PropTypes.func, onKeyDown : PropTypes.func,
onPaste: PropTypes.func.isRequired, onPaste : PropTypes.func.isRequired,
autoFocus: PropTypes.bool, autoFocus : PropTypes.bool,
directMessage : PropTypes.bool,
directMessageRecipient : PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
autoFocus: true, autoFocus : true,
directMessage: false,
}; };
state = { state = {
suggestionsHidden: true, suggestionsHidden : true,
focused: false, focused : false,
selectedSuggestion: 0, selectedSuggestion: 0,
lastToken: null, lastToken : null,
tokenStart: 0, tokenStart : 0,
}; };
onChange = (e) => { onChange = (e) => {
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart); const [tokenStart, token] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
if (token !== null && this.state.lastToken !== token) { if (token !== null && this.state.lastToken !== token) {
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
@ -75,7 +78,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
this.props.onChange(e); this.props.onChange(e);
} };
onKeyDown = (e) => { onKeyDown = (e) => {
const { suggestions, disabled } = this.props; const { suggestions, disabled } = this.props;
@ -92,7 +95,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
return; return;
} }
switch(e.key) { switch (e.key) {
case 'Escape': case 'Escape':
if (suggestions.size === 0 || suggestionsHidden) { if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector('.ui').parentElement.focus(); document.querySelector('.ui').parentElement.focus();
@ -124,7 +127,6 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
e.stopPropagation(); e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
} }
break; break;
} }
@ -133,27 +135,27 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
this.props.onKeyDown(e); this.props.onKeyDown(e);
} };
onBlur = () => { onBlur = () => {
this.setState({ suggestionsHidden: true, focused: false }); this.setState({ suggestionsHidden: true, focused: false });
} };
onFocus = (e) => { onFocus = (e) => {
this.setState({ focused: true }); this.setState({ focused: true });
if (this.props.onFocus) { if (this.props.onFocus) {
this.props.onFocus(e); this.props.onFocus(e);
} }
} };
onSuggestionClick = (e) => { onSuggestionClick = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
e.preventDefault(); e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textarea.focus(); this.textarea.focus();
} };
componentWillReceiveProps (nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false }); this.setState({ suggestionsHidden: false });
} }
@ -161,14 +163,14 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
setTextarea = (c) => { setTextarea = (c) => {
this.textarea = c; this.textarea = c;
} };
onPaste = (e) => { onPaste = (e) => {
if (e.clipboardData && e.clipboardData.files.length === 1) { if (e.clipboardData && e.clipboardData.files.length === 1) {
this.props.onPaste(e.clipboardData.files); this.props.onPaste(e.clipboardData.files);
e.preventDefault(); e.preventDefault();
} }
} };
renderSuggestion = (suggestion, i) => { renderSuggestion = (suggestion, i) => {
const { selectedSuggestion } = this.state; const { selectedSuggestion } = this.state;
@ -186,13 +188,20 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
return ( return (
<div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}> <div
role='button'
tabIndex='0'
key={key}
data-index={i}
className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })}
onMouseDown={this.onSuggestionClick}
>
{inner} {inner}
</div> </div >
); );
} };
render () { render() {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props; const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
const { suggestionsHidden } = this.state; const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' }; const style = { direction: 'ltr' };
@ -202,10 +211,13 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
return [ return [
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'> <div
className='compose-form__autosuggest-wrapper'
key='autosuggest-wrapper'
>
<div className='autosuggest-textarea'> <div className='autosuggest-textarea'>
<label> <label >
<span style={{ display: 'none' }}>{placeholder}</span> <span style={{ display: 'none' }}>{placeholder}</span >
<Textarea <Textarea
inputRef={this.setTextarea} inputRef={this.setTextarea}
@ -223,16 +235,21 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
style={style} style={style}
aria-autocomplete='list' aria-autocomplete='list'
/> />
</label> </label >
</div> </div >
{children} {children}
</div>, </div >,
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'> <div
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> className='autosuggest-textarea__suggestions-wrapper'
key='suggestions-wrapper'
>
<div
className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}
>
{suggestions.map(this.renderSuggestion)} {suggestions.map(this.renderSuggestion)}
</div> </div >
</div>, </div >,
]; ];
} }

View File

@ -2,13 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, show : { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, hide : { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' }, moveLeft : { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
}); });
@ -20,19 +20,19 @@ class ColumnHeader extends React.PureComponent {
}; };
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
title: PropTypes.node, title : PropTypes.node,
icon: PropTypes.string, icon : PropTypes.string,
active: PropTypes.bool, active : PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn : PropTypes.bool,
extraButton: PropTypes.node, extraButton : PropTypes.node,
showBackButton: PropTypes.bool, showBackButton: PropTypes.bool,
children: PropTypes.node, children : PropTypes.node,
pinned: PropTypes.bool, pinned : PropTypes.bool,
placeholder: PropTypes.bool, placeholder : PropTypes.bool,
onPin: PropTypes.func, onPin : PropTypes.func,
onMove: PropTypes.func, onMove : PropTypes.func,
onClick: PropTypes.func, onClick : PropTypes.func,
}; };
state = { state = {
@ -46,41 +46,41 @@ class ColumnHeader extends React.PureComponent {
} else { } else {
this.context.router.history.goBack(); this.context.router.history.goBack();
} }
} };
handleToggleClick = (e) => { handleToggleClick = (e) => {
e.stopPropagation(); e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed, animating: true }); this.setState({ collapsed: !this.state.collapsed, animating: true });
} };
handleTitleClick = () => { handleTitleClick = () => {
this.props.onClick(); this.props.onClick();
} };
handleMoveLeft = () => { handleMoveLeft = () => {
this.props.onMove(-1); this.props.onMove(-1);
} };
handleMoveRight = () => { handleMoveRight = () => {
this.props.onMove(1); this.props.onMove(1);
} };
handleBackClick = () => { handleBackClick = () => {
this.historyBack(); this.historyBack();
} };
handleTransitionEnd = () => { handleTransitionEnd = () => {
this.setState({ animating: false }); this.setState({ animating: false });
} };
handlePin = () => { handlePin = () => {
if (!this.props.pinned) { if (!this.props.pinned) {
this.historyBack(); this.historyBack();
} }
this.props.onPin(); this.props.onPin();
} };
render () { render() {
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder } = this.props; const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder } = this.props;
const { collapsed, animating } = this.state; const { collapsed, animating } = this.state;
@ -105,31 +105,82 @@ class ColumnHeader extends React.PureComponent {
if (children) { if (children) {
extraContent = ( extraContent = (
<div key='extra-content' className='column-header__collapsible__extra'> <div
key='extra-content'
className='column-header__collapsible__extra'
>
{children} {children}
</div> </div >
); );
} }
if (multiColumn && pinned) { if (multiColumn && pinned) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>; pinButton =
(<button
key='pin-button'
className='text-btn column-header__setting-btn'
onClick={this.handlePin}
><Icon
id='times'
/> <FormattedMessage
id='column_header.unpin'
defaultMessage='Unpin'
/></button >);
moveButtons = ( moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'> <div
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button> key='move-buttons'
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button> className='column-header__setting-arrows'
</div> >
<button
title={formatMessage(messages.moveLeft)}
aria-label={formatMessage(messages.moveLeft)}
className='text-btn column-header__setting-btn'
onClick={this.handleMoveLeft}
><Icon
id='chevron-left'
/></button >
<button
title={formatMessage(messages.moveRight)}
aria-label={formatMessage(messages.moveRight)}
className='text-btn column-header__setting-btn'
onClick={this.handleMoveRight}
><Icon
id='chevron-right'
/></button >
</div >
); );
} else if (multiColumn && this.props.onPin) { } else if (multiColumn && this.props.onPin) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; pinButton =
(<button
key='pin-button'
className='text-btn column-header__setting-btn'
onClick={this.handlePin}
><Icon
id='plus'
/> <FormattedMessage
id='column_header.pin'
defaultMessage='Pin'
/>
</button >);
} }
if (!pinned && (multiColumn || showBackButton)) { if (!pinned && (multiColumn || showBackButton)) {
backButton = ( backButton = (
<button onClick={this.handleBackClick} className='column-header__back-button'> <button
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> onClick={this.handleBackClick}
<FormattedMessage id='column_back_button.label' defaultMessage='Back' /> className='column-header__back-button'
</button> >
<Icon
id='chevron-left'
className='column-back-button__icon'
fixedWidth
/>
<FormattedMessage
id='column_back_button.label'
defaultMessage='Back'
/>
</button >
); );
} }
@ -143,7 +194,16 @@ class ColumnHeader extends React.PureComponent {
} }
if (children || (multiColumn && this.props.onPin)) { if (children || (multiColumn && this.props.onPin)) {
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>; collapseButton =
(<button
className={collapsibleButtonClassName}
title={formatMessage(collapsed ? messages.show : messages.hide)}
aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
aria-pressed={collapsed ? 'false' : 'true'}
onClick={this.handleToggleClick}
>
<Icon id='sliders' />
</button >);
} }
const hasTitle = icon && title; const hasTitle = icon && title;
@ -153,9 +213,13 @@ class ColumnHeader extends React.PureComponent {
<h1 className={buttonClassName}> <h1 className={buttonClassName}>
{hasTitle && ( {hasTitle && (
<button onClick={this.handleTitleClick}> <button onClick={this.handleTitleClick}>
<Icon id={icon} fixedWidth className='column-header__icon' /> <Icon
id={icon}
fixedWidth
className='column-header__icon'
/>
{title} {title}
</button> </button >
)} )}
{!hasTitle && backButton} {!hasTitle && backButton}
@ -164,15 +228,19 @@ class ColumnHeader extends React.PureComponent {
{hasTitle && backButton} {hasTitle && backButton}
{extraButton} {extraButton}
{collapseButton} {collapseButton}
</div> </div >
</h1> </h1 >
<div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}> <div
className={collapsibleClassName}
tabIndex={collapsed ? -1 : null}
onTransitionEnd={this.handleTransitionEnd}
>
<div className='column-header__collapsible-inner'> <div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent} {(!collapsed || animating) && collapsedContent}
</div> </div >
</div> </div >
</div> </div >
); );
if (multiColumn || placeholder) { if (multiColumn || placeholder) {

View File

@ -6,12 +6,12 @@ import { autoPlayGif } from 'mastodon/initial_state';
export default class DisplayName extends React.PureComponent { export default class DisplayName extends React.PureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account : ImmutablePropTypes.map.isRequired,
others: ImmutablePropTypes.list, others : ImmutablePropTypes.list,
localDomain: PropTypes.string, localDomain: PropTypes.string,
}; };
_updateEmojis () { _updateEmojis() {
const node = this.node; const node = this.node;
if (!node || autoPlayGif) { if (!node || autoPlayGif) {
@ -32,33 +32,40 @@ export default class DisplayName extends React.PureComponent {
} }
} }
componentDidMount () { componentDidMount() {
this._updateEmojis(); this._updateEmojis();
} }
componentDidUpdate () { componentDidUpdate() {
this._updateEmojis(); this._updateEmojis();
} }
handleEmojiMouseEnter = ({ target }) => { handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original'); target.src = target.getAttribute('data-original');
} };
handleEmojiMouseLeave = ({ target }) => { handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static'); target.src = target.getAttribute('data-static');
} };
setRef = (c) => { setRef = (c) => {
this.node = c; this.node = c;
} };
render () { render() {
const { others, localDomain } = this.props; const { others, localDomain } = this.props;
let displayName, suffix, account; let displayName, suffix, account;
if (others && others.size > 1) { if (others && others.size > 1) {
displayName = others.take(2).map(a => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).reduce((prev, cur) => [prev, ', ', cur]); displayName = others.take(2).map(a =>
(<bdi key={a.get('id')}>
<strong
className='display-name__html'
dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
/>
</bdi >)).reduce((prev, cur) => [prev, ', ', cur]);
if (others.size - 2 > 0) { if (others.size - 2 > 0) {
suffix = `+${others.size - 2}`; suffix = `+${others.size - 2}`;
@ -76,14 +83,22 @@ export default class DisplayName extends React.PureComponent {
acct = `${acct}@${localDomain}`; acct = `${acct}@${localDomain}`;
} }
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>; displayName = <bdi ><strong
suffix = <span className='display-name__account'>@{acct}</span>; className='display-name__html'
dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
/>
</bdi >;
suffix = <span className='display-name__account'>@{acct}</span >;
} }
return ( return (
<span className='display-name' ref={this.setRef}> <span
className='display-name'
ref={this.setRef}
>
{displayName} {suffix} {displayName} {suffix}
</span> </span >
); );
} }

View File

@ -6,41 +6,41 @@ import Icon from 'mastodon/components/icon';
export default class IconButton extends React.PureComponent { export default class IconButton extends React.PureComponent {
static propTypes = { static propTypes = {
className: PropTypes.string, className : PropTypes.string,
title: PropTypes.string.isRequired, title : PropTypes.string.isRequired,
icon: PropTypes.string.isRequired, icon : PropTypes.string.isRequired,
onClick: PropTypes.func, onClick : PropTypes.func,
onMouseDown: PropTypes.func, onMouseDown: PropTypes.func,
onKeyDown: PropTypes.func, onKeyDown : PropTypes.func,
onKeyPress: PropTypes.func, onKeyPress : PropTypes.func,
size: PropTypes.number, size : PropTypes.number,
active: PropTypes.bool, active : PropTypes.bool,
pressed: PropTypes.bool, pressed : PropTypes.bool,
expanded: PropTypes.bool, expanded : PropTypes.bool,
style: PropTypes.object, style : PropTypes.object,
activeStyle: PropTypes.object, activeStyle: PropTypes.object,
disabled: PropTypes.bool, disabled : PropTypes.bool,
inverted: PropTypes.bool, inverted : PropTypes.bool,
animate: PropTypes.bool, animate : PropTypes.bool,
overlay: PropTypes.bool, overlay : PropTypes.bool,
tabIndex: PropTypes.string, tabIndex : PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
size: 18, size : 18,
active: false, active : false,
disabled: false, disabled: false,
animate: false, animate : false,
overlay: false, overlay : false,
tabIndex: '0', tabIndex: '0',
}; };
state = { state = {
activate: false, activate : false,
deactivate: false, deactivate: false,
} };
componentWillReceiveProps (nextProps) { componentWillReceiveProps(nextProps) {
if (!nextProps.animate) return; if (!nextProps.animate) return;
if (this.props.active && !nextProps.active) { if (this.props.active && !nextProps.active) {
@ -56,32 +56,32 @@ export default class IconButton extends React.PureComponent {
if (!this.props.disabled) { if (!this.props.disabled) {
this.props.onClick(e); this.props.onClick(e);
} }
} };
handleKeyPress = (e) => { handleKeyPress = (e) => {
if (this.props.onKeyPress && !this.props.disabled) { if (this.props.onKeyPress && !this.props.disabled) {
this.props.onKeyPress(e); this.props.onKeyPress(e);
} }
} };
handleMouseDown = (e) => { handleMouseDown = (e) => {
if (!this.props.disabled && this.props.onMouseDown) { if (!this.props.disabled && this.props.onMouseDown) {
this.props.onMouseDown(e); this.props.onMouseDown(e);
} }
} };
handleKeyDown = (e) => { handleKeyDown = (e) => {
if (!this.props.disabled && this.props.onKeyDown) { if (!this.props.disabled && this.props.onKeyDown) {
this.props.onKeyDown(e); this.props.onKeyDown(e);
} }
} };
render () { render() {
const style = { const style = {
fontSize: `${this.props.size}px`, // fontSize: `${this.props.size}px`,
width: `${this.props.size * 1.28571429}px`, // width: `${this.props.size * 1.28571429}px`,
height: `${this.props.size * 1.28571429}px`, // height: `${this.props.size * 1.28571429}px`,
lineHeight: `${this.props.size}px`, // lineHeight: `${this.props.size}px`,
...this.props.style, ...this.props.style,
...(this.props.active ? this.props.activeStyle : {}), ...(this.props.active ? this.props.activeStyle : {}),
}; };
@ -128,8 +128,12 @@ export default class IconButton extends React.PureComponent {
tabIndex={tabIndex} tabIndex={tabIndex}
disabled={disabled} disabled={disabled}
> >
<Icon id={icon} fixedWidth aria-hidden='true' /> <Icon
</button> id={icon}
fixedWidth
aria-hidden='true'
/>
</button >
); );
} }

View File

@ -10,17 +10,17 @@ import StatusContent from './status_content';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list'; import AttachmentList from './attachment_list';
import Card from '../features/status/components/card'; import Card from '../features/status/components/card';
import { injectIntl, FormattedMessage } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; import { Audio, MediaGallery, Video } from '../features/ui/util/async-components';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import { displayMedia } from '../initial_state'; import { displayMedia, isStaff } from '../initial_state';
// We use the component (and not the container) since we do not want // We use the component (and not the container) since we do not want
// to use the progress bar to show download progress // to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle'; import Bundle from '../features/ui/components/bundle';
import imageShowThread from '../../images/icon_reply.svg';
export const textForScreenReader = (intl, status, rebloggedByText = false) => { export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']); const displayName = status.getIn(['account', 'display_name']);
@ -59,33 +59,37 @@ class Status extends ImmutablePureComponent {
}; };
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status : ImmutablePropTypes.map,
account: ImmutablePropTypes.map, account : ImmutablePropTypes.map,
otherAccounts: ImmutablePropTypes.list, otherAccounts : ImmutablePropTypes.list,
onClick: PropTypes.func, onClick : PropTypes.func,
onReply: PropTypes.func, onReply : PropTypes.func,
onFavourite: PropTypes.func, onFavourite : PropTypes.func,
onReblog: PropTypes.func, onReblog : PropTypes.func,
onDelete: PropTypes.func, onDelete : PropTypes.func,
onDirect: PropTypes.func, onDirect : PropTypes.func,
onMention: PropTypes.func, onMention : PropTypes.func,
onPin: PropTypes.func, onPin : PropTypes.func,
onOpenMedia: PropTypes.func, onOpenMedia : PropTypes.func,
onOpenVideo: PropTypes.func, onOpenVideo : PropTypes.func,
onBlock: PropTypes.func, onBlock : PropTypes.func,
onEmbed: PropTypes.func, onEmbed : PropTypes.func,
onHeightChange: PropTypes.func, onHeightChange : PropTypes.func,
onToggleHidden: PropTypes.func, onToggleHidden : PropTypes.func,
muted: PropTypes.bool, muted : PropTypes.bool,
hidden: PropTypes.bool, hidden : PropTypes.bool,
unread: PropTypes.bool, unread : PropTypes.bool,
onMoveUp: PropTypes.func, onMoveUp : PropTypes.func,
onMoveDown: PropTypes.func, onMoveDown : PropTypes.func,
showThread: PropTypes.bool, showThread : PropTypes.bool,
getScrollPosition: PropTypes.func, threadsCompile : PropTypes.bool,
getScrollPosition : PropTypes.func,
updateScrollBottom: PropTypes.func, updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func, cacheMediaWidth : PropTypes.func,
cachedMediaWidth: PropTypes.number, cachedMediaWidth : PropTypes.number,
};
static defaultProps = {
threadsCompile: true,
}; };
// Avoid checking props that are functions (and whose equality will always // Avoid checking props that are functions (and whose equality will always
@ -99,15 +103,26 @@ class Status extends ImmutablePureComponent {
state = { state = {
showMedia: defaultMediaVisibility(this.props.status), showMedia: defaultMediaVisibility(this.props.status),
statusId: undefined, statusId : undefined,
}; };
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return {
showMedia: defaultMediaVisibility(nextProps.status),
statusId : nextProps.status.get('id'),
};
} else {
return null;
}
}
// Track height changes we know about to compensate scrolling // Track height changes we know about to compensate scrolling
componentDidMount () { componentDidMount() {
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card'); this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
} }
getSnapshotBeforeUpdate () { getSnapshotBeforeUpdate() {
if (this.props.getScrollPosition) { if (this.props.getScrollPosition) {
return this.props.getScrollPosition(); return this.props.getScrollPosition();
} else { } else {
@ -115,19 +130,8 @@ class Status extends ImmutablePureComponent {
} }
} }
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return {
showMedia: defaultMediaVisibility(nextProps.status),
statusId: nextProps.status.get('id'),
};
} else {
return null;
}
}
// Compensate height changes // Compensate height changes
componentDidUpdate (prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card'); const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
if (doShowCard && !this.didShowCard) { if (doShowCard && !this.didShowCard) {
@ -154,7 +158,7 @@ class Status extends ImmutablePureComponent {
handleToggleMediaVisibility = () => { handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia }); this.setState({ showMedia: !this.state.showMedia });
} };
handleClick = () => { handleClick = () => {
if (this.props.onClick) { if (this.props.onClick) {
@ -168,7 +172,7 @@ class Status extends ImmutablePureComponent {
const { status } = this.props; const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
} };
handleExpandClick = (e) => { handleExpandClick = (e) => {
if (this.props.onClick) { if (this.props.onClick) {
@ -184,7 +188,7 @@ class Status extends ImmutablePureComponent {
const { status } = this.props; const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
} }
} };
handleAccountClick = (e) => { handleAccountClick = (e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
@ -192,27 +196,36 @@ class Status extends ImmutablePureComponent {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/accounts/${id}`); this.context.router.history.push(`/accounts/${id}`);
} }
} };
handleExpandedToggle = () => { handleExpandedToggle = () => {
this.props.onToggleHidden(this._properStatus()); this.props.onToggleHidden(this._properStatus());
}; };
renderLoadingMediaGallery () { renderLoadingMediaGallery() {
return <div className='media-gallery' style={{ height: '110px' }} />; return (<div
className='media-gallery'
style={{ height: '110px' }}
/>);
} }
renderLoadingVideoPlayer () { renderLoadingVideoPlayer() {
return <div className='video-player' style={{ height: '110px' }} />; return (<div
className='video-player'
style={{ height: '110px' }}
/>);
} }
renderLoadingAudioPlayer () { renderLoadingAudioPlayer() {
return <div className='audio-player' style={{ height: '110px' }} />; return (<div
className='audio-player'
style={{ height: '110px' }}
/>);
} }
handleOpenVideo = (media, startTime) => { handleOpenVideo = (media, startTime) => {
this.props.onOpenVideo(media, startTime); this.props.onOpenVideo(media, startTime);
} };
handleHotkeyOpenMedia = e => { handleHotkeyOpenMedia = e => {
const { onOpenMedia, onOpenVideo } = this.props; const { onOpenMedia, onOpenVideo } = this.props;
@ -229,51 +242,51 @@ class Status extends ImmutablePureComponent {
onOpenMedia(status.get('media_attachments'), 0); onOpenMedia(status.get('media_attachments'), 0);
} }
} }
} };
handleHotkeyReply = e => { handleHotkeyReply = e => {
e.preventDefault(); e.preventDefault();
this.props.onReply(this._properStatus(), this.context.router.history); this.props.onReply(this._properStatus(), this.context.router.history);
} };
handleHotkeyFavourite = () => { handleHotkeyFavourite = () => {
this.props.onFavourite(this._properStatus()); this.props.onFavourite(this._properStatus());
} };
handleHotkeyBoost = e => { handleHotkeyBoost = e => {
this.props.onReblog(this._properStatus(), e); this.props.onReblog(this._properStatus(), e);
} };
handleHotkeyMention = e => { handleHotkeyMention = e => {
e.preventDefault(); e.preventDefault();
this.props.onMention(this._properStatus().get('account'), this.context.router.history); this.props.onMention(this._properStatus().get('account'), this.context.router.history);
} };
handleHotkeyOpen = () => { handleHotkeyOpen = () => {
this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`); this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`);
} };
handleHotkeyOpenProfile = () => { handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`); this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`);
} };
handleHotkeyMoveUp = e => { handleHotkeyMoveUp = e => {
this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured')); this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
} };
handleHotkeyMoveDown = e => { handleHotkeyMoveDown = e => {
this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured')); this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
} };
handleHotkeyToggleHidden = () => { handleHotkeyToggleHidden = () => {
this.props.onToggleHidden(this._properStatus()); this.props.onToggleHidden(this._properStatus());
} };
handleHotkeyToggleSensitive = () => { handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility(); this.handleToggleMediaVisibility();
} };
_properStatus () { _properStatus() {
const { status } = this.props; const { status } = this.props;
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
@ -285,9 +298,9 @@ class Status extends ImmutablePureComponent {
handleRef = c => { handleRef = c => {
this.node = c; this.node = c;
} };
render () { render() {
let media = null; let media = null;
let statusAvatar, prepend, rebloggedByText; let statusAvatar, prepend, rebloggedByText;
@ -300,63 +313,102 @@ class Status extends ImmutablePureComponent {
} }
const handlers = this.props.muted ? {} : { const handlers = this.props.muted ? {} : {
reply: this.handleHotkeyReply, reply : this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite, favourite : this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost, boost : this.handleHotkeyBoost,
mention: this.handleHotkeyMention, mention : this.handleHotkeyMention,
open: this.handleHotkeyOpen, open : this.handleHotkeyOpen,
openProfile: this.handleHotkeyOpenProfile, openProfile : this.handleHotkeyOpenProfile,
moveUp: this.handleHotkeyMoveUp, moveUp : this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown, moveDown : this.handleHotkeyMoveDown,
toggleHidden: this.handleHotkeyToggleHidden, toggleHidden : this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive, toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia, openMedia : this.handleHotkeyOpenMedia,
}; };
if (hidden) { if (hidden) {
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex='0'> <div
ref={this.handleRef}
className={classNames('status__wrapper', { focusable: !this.props.muted })}
tabIndex='0'
>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')} {status.get('content')}
</div> </div >
</HotKeys> </HotKeys >
); );
} }
if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) { if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) {
const minHandlers = this.props.muted ? {} : { const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp, moveUp : this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown, moveDown: this.handleHotkeyMoveDown,
}; };
return ( return (
<HotKeys handlers={minHandlers}> <HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}> <div
<FormattedMessage id='status.filtered' defaultMessage='Filtered' /> className='status__wrapper status__wrapper--filtered focusable'
</div> tabIndex='0'
</HotKeys> ref={this.handleRef}
>
<FormattedMessage
id='status.filtered'
defaultMessage='Filtered'
/>
</div >
</HotKeys >
); );
} }
if (featured) { if (featured) {
prepend = ( prepend = (
<div className='status__prepend'> <div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div> <div className='status__prepend-icon-wrapper'><Icon
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' /> id='thumb-tack'
</div> className='status__prepend-icon'
fixedWidth
/></div >
<FormattedMessage
id='status.pinned'
defaultMessage='Pinned toot'
/>
</div >
); );
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { } else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) }; const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
prepend = ( prepend = (
<div className='status__prepend'> <div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div> <div className='status__prepend-icon-wrapper'><Icon
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} /> id='retweet'
</div> className='status__prepend-icon'
fixedWidth
/>
</div >
<FormattedMessage
id='status.reblogged_by'
defaultMessage='{name} boosted'
values={{
name: <a
onClick={this.handleAccountClick}
data-id={status.getIn(['account', 'id'])}
href={status.getIn(['account', 'url'])}
className='status__display-name muted'
>
<bdi ><strong dangerouslySetInnerHTML={display_name_html} /></bdi >
</a >,
}}
/>
</div >
); );
rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: status.getIn(['account', 'acct']) }); rebloggedByText = intl.formatMessage({
id : 'status.reblogged_by',
defaultMessage: '{name} boosted',
}, { name: status.getIn(['account', 'acct']) });
account = status.get('account'); account = status.get('account');
status = status.get('reblog'); status = status.get('reblog');
@ -374,7 +426,10 @@ class Status extends ImmutablePureComponent {
const attachment = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
media = ( media = (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} > <Bundle
fetchComponent={Audio}
loading={this.renderLoadingAudioPlayer}
>
{Component => ( {Component => (
<Component <Component
src={attachment.get('url')} src={attachment.get('url')}
@ -384,13 +439,16 @@ class Status extends ImmutablePureComponent {
height={70} height={70}
/> />
)} )}
</Bundle> </Bundle >
); );
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
media = ( media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > <Bundle
fetchComponent={Video}
loading={this.renderLoadingVideoPlayer}
>
{Component => ( {Component => (
<Component <Component
preview={attachment.get('preview_url')} preview={attachment.get('preview_url')}
@ -407,11 +465,14 @@ class Status extends ImmutablePureComponent {
onToggleVisibility={this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
/> />
)} )}
</Bundle> </Bundle >
); );
} else { } else {
media = ( media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}> <Bundle
fetchComponent={MediaGallery}
loading={this.renderLoadingMediaGallery}
>
{Component => ( {Component => (
<Component <Component
media={status.get('media_attachments')} media={status.get('media_attachments')}
@ -424,7 +485,7 @@ class Status extends ImmutablePureComponent {
onToggleVisibility={this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
/> />
)} )}
</Bundle> </Bundle >
); );
} }
} else if (status.get('spoiler_text').length === 0 && status.get('card')) { } else if (status.get('spoiler_text').length === 0 && status.get('card')) {
@ -440,46 +501,126 @@ class Status extends ImmutablePureComponent {
} }
if (otherAccounts && otherAccounts.size > 0) { if (otherAccounts && otherAccounts.size > 0) {
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />; statusAvatar = (<AvatarComposite
accounts={otherAccounts}
size={48}
/>);
} else if (account === undefined || account === null) { } else if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={48} />; statusAvatar = (<Avatar
account={status.get('account')}
size={48}
/>);
} else { } else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />; statusAvatar = (<AvatarOverlay
account={status.get('account')}
friend={account}
/>);
} }
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}> <div
className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, {
'status__wrapper-reply': !!status.get('in_reply_to_id'),
read : unread === false,
focusable : !this.props.muted,
})}
tabIndex={this.props.muted ? null : 0}
data-featured={featured ? 'true' : null}
aria-label={textForScreenReader(intl, status, rebloggedByText)}
ref={this.handleRef}
>
{prepend} {prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}> <div
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' /> className={classNames('status', `status-${status.get('visibility')}`, {
'status-reply': !!status.get('in_reply_to_id'),
muted : this.props.muted,
read : unread === false,
})}
data-id={status.get('id')}
>
<div
className='status__expand'
onClick={this.handleExpandClick}
role='presentation'
/>
<div className='status__info'> <div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'> {isStaff && (<div className='administrate-stuff pull-left'>
<i className='fa fa-gears' />
</div >
)}
<a
href={status.get('url')}
className='status__relative-time'
target='_blank'
rel='noopener noreferrer'
><RelativeTimestamp timestamp={status.get('created_at')} /></a >
<a
onClick={this.handleAccountClick}
data-id={status.getIn(['account', 'id'])}
href={status.getIn(['account', 'url'])}
title={status.getIn(['account', 'acct'])}
className='status__display-name'
target='_blank'
rel='noopener noreferrer'
>
<div className='status__avatar'> <div className='status__avatar'>
{statusAvatar} {statusAvatar}
</div> </div >
<DisplayName account={status.get('account')} others={otherAccounts} /> <DisplayName
</a> account={status.get('account')}
</div> others={otherAccounts}
/>
</a >
</div >
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable /> <StatusContent
status={status}
onClick={this.handleClick}
expanded={!status.get('hidden')}
onExpandedToggle={this.handleExpandedToggle}
collapsable
/>
{media} {media}
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && ( {showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
<button className='status__content__read-more-button' onClick={this.handleClick}> <button
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' /> className='status__content__read-more-button'
</button> onClick={this.handleClick}
)} >
<StatusActionBar status={status} account={account} {...other} /> <FormattedMessage
</div> id='status.show_thread'
</div> defaultMessage='Show thread'
</HotKeys> />
<img
src={imageShowThread}
alt='=> '
/>
</button >
)}
{/*<div className='well'>*/}
{/* {status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) &&*/}
{/* <p > oui je cause tout seul, c'est un thread</p >*/}
{/* }*/}
{/* {this.props.threadsCompile &&*/}
{/* <p > les threads sont en mode compilés</p >*/}
{/* }*/}
{/*</div >*/}
<StatusActionBar
status={status}
account={account} {...other}
/>
</div >
</div >
</HotKeys >
); );
} }

View File

@ -24,10 +24,10 @@ import Icon from 'mastodon/components/icon';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d'; const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, placeholder : { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' }, spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }, publish : { id: 'compose_form.publish', defaultMessage: 'Toot' },
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' }, publishLoud : { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
}); });
export default @injectIntl export default @injectIntl
@ -38,44 +38,46 @@ class ComposeForm extends ImmutablePureComponent {
}; };
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
text: PropTypes.string.isRequired, text : PropTypes.string.isRequired,
suggestions: ImmutablePropTypes.list, suggestions : ImmutablePropTypes.list,
spoiler: PropTypes.bool, spoiler : PropTypes.bool,
privacy: PropTypes.string, privacy : PropTypes.string,
spoilerText: PropTypes.string, spoilerText : PropTypes.string,
focusDate: PropTypes.instanceOf(Date), focusDate : PropTypes.instanceOf(Date),
caretPosition: PropTypes.number, caretPosition : PropTypes.number,
preselectDate: PropTypes.instanceOf(Date), maxTootCharsLimit : PropTypes.number,
isSubmitting: PropTypes.bool, preselectDate : PropTypes.instanceOf(Date),
isChangingUpload: PropTypes.bool, isSubmitting : PropTypes.bool,
isUploading: PropTypes.bool, isChangingUpload : PropTypes.bool,
onChange: PropTypes.func.isRequired, isUploading : PropTypes.bool,
onSubmit: PropTypes.func.isRequired, onChange : PropTypes.func.isRequired,
onClearSuggestions: PropTypes.func.isRequired, onSubmit : PropTypes.func.isRequired,
onFetchSuggestions: PropTypes.func.isRequired, onClearSuggestions : PropTypes.func.isRequired,
onFetchSuggestions : PropTypes.func.isRequired,
onSuggestionSelected: PropTypes.func.isRequired, onSuggestionSelected: PropTypes.func.isRequired,
onChangeSpoilerText: PropTypes.func.isRequired, onChangeSpoilerText : PropTypes.func.isRequired,
onPaste: PropTypes.func.isRequired, onPaste : PropTypes.func.isRequired,
onPickEmoji: PropTypes.func.isRequired, onPickEmoji : PropTypes.func.isRequired,
showSearch: PropTypes.bool, showSearch : PropTypes.bool,
anyMedia: PropTypes.bool, anyMedia : PropTypes.bool,
singleColumn: PropTypes.bool, singleColumn : PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
showSearch: false, showSearch : false,
maxTootCharsLimit: 7777,
}; };
handleChange = (e) => { handleChange = (e) => {
this.props.onChange(e.target.value); this.props.onChange(e.target.value);
} };
handleKeyDown = (e) => { handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.handleSubmit(); this.handleSubmit();
} }
} };
handleSubmit = () => { handleSubmit = () => {
if (this.props.text !== this.autosuggestTextarea.textarea.value) { if (this.props.text !== this.autosuggestTextarea.textarea.value) {
@ -88,32 +90,32 @@ class ComposeForm extends ImmutablePureComponent {
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props; const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
const fulltext = [this.props.spoilerText, countableText(this.props.text)].join(''); const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('');
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) { if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > this.props.maxTootCharsLimit || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
return; return;
} }
this.props.onSubmit(this.context.router ? this.context.router.history : null); this.props.onSubmit(this.context.router ? this.context.router.history : null);
} };
onSuggestionsClearRequested = () => { onSuggestionsClearRequested = () => {
this.props.onClearSuggestions(); this.props.onClearSuggestions();
} };
onSuggestionsFetchRequested = (token) => { onSuggestionsFetchRequested = (token) => {
this.props.onFetchSuggestions(token); this.props.onFetchSuggestions(token);
} };
onSuggestionSelected = (tokenStart, token, value) => { onSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['text']); this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
} };
onSpoilerSuggestionSelected = (tokenStart, token, value) => { onSpoilerSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']); this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
} };
handleChangeSpoilerText = (e) => { handleChangeSpoilerText = (e) => {
this.props.onChangeSpoilerText(e.target.value); this.props.onChangeSpoilerText(e.target.value);
} };
handleFocus = () => { handleFocus = () => {
if (this.composeForm && !this.props.singleColumn) { if (this.composeForm && !this.props.singleColumn) {
@ -122,9 +124,9 @@ class ComposeForm extends ImmutablePureComponent {
this.composeForm.scrollIntoView(); this.composeForm.scrollIntoView();
} }
} }
} };
componentDidUpdate (prevProps) { componentDidUpdate(prevProps) {
// This statement does several things: // This statement does several things:
// - If we're beginning a reply, and, // - If we're beginning a reply, and,
// - Replying to zero or one users, places the cursor at the end of the textbox. // - Replying to zero or one users, places the cursor at the end of the textbox.
@ -146,7 +148,7 @@ class ComposeForm extends ImmutablePureComponent {
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd); this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
this.autosuggestTextarea.textarea.focus(); this.autosuggestTextarea.textarea.focus();
} else if(prevProps.isSubmitting && !this.props.isSubmitting) { } else if (prevProps.isSubmitting && !this.props.isSubmitting) {
this.autosuggestTextarea.textarea.focus(); this.autosuggestTextarea.textarea.focus();
} else if (this.props.spoiler !== prevProps.spoiler) { } else if (this.props.spoiler !== prevProps.spoiler) {
if (this.props.spoiler) { if (this.props.spoiler) {
@ -159,11 +161,11 @@ class ComposeForm extends ImmutablePureComponent {
setAutosuggestTextarea = (c) => { setAutosuggestTextarea = (c) => {
this.autosuggestTextarea = c; this.autosuggestTextarea = c;
} };
setSpoilerText = (c) => { setSpoilerText = (c) => {
this.spoilerText = c; this.spoilerText = c;
} };
setRef = c => { setRef = c => {
this.composeForm = c; this.composeForm = c;
@ -175,17 +177,18 @@ class ComposeForm extends ImmutablePureComponent {
const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]); const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
this.props.onPickEmoji(position, data, needsSpace); this.props.onPickEmoji(position, data, needsSpace);
} };
render () { render() {
const { intl, onPaste, showSearch, anyMedia } = this.props; const { intl, onPaste, showSearch, anyMedia } = this.props;
const disabled = this.props.isSubmitting; const disabled = this.props.isSubmitting;
const text = [this.props.spoilerText, countableText(this.props.text)].join(''); const text = [this.props.spoilerText, countableText(this.props.text)].join('');
const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia); const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > this.props.maxTootCharsLimit || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
let publishText = ''; let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') { if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>; publishText =
<span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span >;
} else { } else {
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
} }
@ -196,7 +199,10 @@ class ComposeForm extends ImmutablePureComponent {
<ReplyIndicatorContainer /> <ReplyIndicatorContainer />
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef}> <div
className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}
ref={this.setRef}
>
<AutosuggestInput <AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)} placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText} value={this.props.spoilerText}
@ -212,7 +218,7 @@ class ComposeForm extends ImmutablePureComponent {
id='cw-spoiler-input' id='cw-spoiler-input'
className='spoiler-input__input' className='spoiler-input__input'
/> />
</div> </div >
<AutosuggestTextarea <AutosuggestTextarea
ref={this.setAutosuggestTextarea} ref={this.setAutosuggestTextarea}
@ -233,8 +239,8 @@ class ComposeForm extends ImmutablePureComponent {
<div className='compose-form__modifiers'> <div className='compose-form__modifiers'>
<UploadFormContainer /> <UploadFormContainer />
<PollFormContainer /> <PollFormContainer />
</div> </div >
</AutosuggestTextarea> </AutosuggestTextarea >
<div className='compose-form__buttons-wrapper'> <div className='compose-form__buttons-wrapper'>
<div className='compose-form__buttons'> <div className='compose-form__buttons'>
@ -242,14 +248,23 @@ class ComposeForm extends ImmutablePureComponent {
<PollButtonContainer /> <PollButtonContainer />
<PrivacyDropdownContainer /> <PrivacyDropdownContainer />
<SpoilerButtonContainer /> <SpoilerButtonContainer />
</div> </div >
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div> <div className='character-counter__wrapper'><CharacterCounter
</div> max={this.props.maxTootCharsLimit}
text={text}
/></div >
</div >
<div className='compose-form__publish'> <div className='compose-form__publish'>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabledButton} block /></div> <div className='compose-form__publish-button-wrapper'><Button
</div> text={publishText}
</div> onClick={this.handleSubmit}
disabled={disabledButton}
block
/></div >
</div >
</div >
); );
} }

View File

@ -11,32 +11,57 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
export default class NavigationBar extends ImmutablePureComponent { export default class NavigationBar extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account : ImmutablePropTypes.map.isRequired,
onLogout: PropTypes.func.isRequired, onLogout: PropTypes.func.isRequired,
onClose: PropTypes.func, onClose : PropTypes.func,
}; };
render () { render() {
return ( return (
<div className='navigation-bar'> <div className='navigation-bar'>
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> <Permalink
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> href={this.props.account.get('url')}
<Avatar account={this.props.account} size={48} /> to={`/accounts/${this.props.account.get('id')}`}
</Permalink> >
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span >
<Avatar
account={this.props.account}
size={48}
/>
</Permalink >
<div className='navigation-bar__profile'> <div className='navigation-bar__profile'>
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> <Permalink
<strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong> href={this.props.account.get('url')}
</Permalink> to={`/accounts/${this.props.account.get('id')}`}
>
<strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong >
</Permalink >
<a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a> <a
</div> href='/settings/profile'
className='navigation-bar__profile-edit'
>
<i className='fa fa-pencil'></i >
<FormattedMessage
id='navigation_bar.edit_profile'
defaultMessage='Edit profile'
/></a >
</div >
<div className='navigation-bar__actions'> <div className='navigation-bar__actions'>
<IconButton className='close' title='' icon='close' onClick={this.props.onClose} /> <IconButton
<ActionBar account={this.props.account} onLogout={this.props.onLogout} /> className='close'
</div> title=''
</div> icon='close'
onClick={this.props.onClose}
/>
<ActionBar
account={this.props.account}
onLogout={this.props.onLogout}
/>
</div >
</div >
); );
} }

View File

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button'; import IconButton from 'mastodon/components/icon_button';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import AutosuggestInput from 'mastodon/components/autosuggest_input'; import AutosuggestInput from 'mastodon/components/autosuggest_input';
@ -56,19 +56,19 @@ class Option extends React.PureComponent {
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
this.handleToggleMultiple(e); this.handleToggleMultiple(e);
} }
} };
onSuggestionsClearRequested = () => { onSuggestionsClearRequested = () => {
this.props.onClearSuggestions(); this.props.onClearSuggestions();
} };
onSuggestionsFetchRequested = (token) => { onSuggestionsFetchRequested = (token) => {
this.props.onFetchSuggestions(token); this.props.onFetchSuggestions(token);
} };
onSuggestionSelected = (tokenStart, token, value) => { onSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]); this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
} };
render () { render () {
const { isPollMultiple, title, index, intl } = this.props; const { isPollMultiple, title, index, intl } = this.props;

View File

@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import StatusContent from 'mastodon/components/status_content'; import StatusContent from 'mastodon/components/status_content';
import AttachmentList from 'mastodon/components/attachment_list'; import AttachmentList from 'mastodon/components/attachment_list';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import AvatarComposite from 'mastodon/components/avatar_composite'; import AvatarComposite from 'mastodon/components/avatar_composite';
import Permalink from 'mastodon/components/permalink'; import Permalink from 'mastodon/components/permalink';
@ -15,12 +15,12 @@ import { autoPlayGif } from 'mastodon/initial_state';
import classNames from 'classnames'; import classNames from 'classnames';
const messages = defineMessages({ const messages = defineMessages({
more: { id: 'status.more', defaultMessage: 'More' }, more : { id: 'status.more', defaultMessage: 'More' },
open: { id: 'conversation.open', defaultMessage: 'View conversation' }, open : { id: 'conversation.open', defaultMessage: 'View conversation' },
reply: { id: 'status.reply', defaultMessage: 'Reply' }, reply : { id: 'status.reply', defaultMessage: 'Reply' },
markAsRead: { id: 'conversation.mark_as_read', defaultMessage: 'Mark as read' }, markAsRead : { id: 'conversation.mark_as_read', defaultMessage: 'Mark as read' },
delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' }, delete : { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, muteConversation : { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
}); });
@ -33,17 +33,17 @@ class Conversation extends ImmutablePureComponent {
static propTypes = { static propTypes = {
conversationId: PropTypes.string.isRequired, conversationId: PropTypes.string.isRequired,
accounts: ImmutablePropTypes.list.isRequired, accounts : ImmutablePropTypes.list.isRequired,
lastStatus: ImmutablePropTypes.map, lastStatus : ImmutablePropTypes.map,
unread:PropTypes.bool.isRequired, unread : PropTypes.bool.isRequired,
onMoveUp: PropTypes.func, onMoveUp : PropTypes.func,
onMoveDown: PropTypes.func, onMoveDown : PropTypes.func,
markRead: PropTypes.func.isRequired, markRead : PropTypes.func.isRequired,
delete: PropTypes.func.isRequired, delete : PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
}; };
_updateEmojis () { _updateEmojis() {
const node = this.namesNode; const node = this.namesNode;
if (!node || autoPlayGif) { if (!node || autoPlayGif) {
@ -64,21 +64,21 @@ class Conversation extends ImmutablePureComponent {
} }
} }
componentDidMount () { componentDidMount() {
this._updateEmojis(); this._updateEmojis();
} }
componentDidUpdate () { componentDidUpdate() {
this._updateEmojis(); this._updateEmojis();
} }
handleEmojiMouseEnter = ({ target }) => { handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original'); target.src = target.getAttribute('data-original');
} };
handleEmojiMouseLeave = ({ target }) => { handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static'); target.src = target.getAttribute('data-static');
} };
handleClick = () => { handleClick = () => {
if (!this.context.router) { if (!this.context.router) {
@ -92,41 +92,41 @@ class Conversation extends ImmutablePureComponent {
} }
this.context.router.history.push(`/statuses/${lastStatus.get('id')}`); this.context.router.history.push(`/statuses/${lastStatus.get('id')}`);
} };
handleMarkAsRead = () => { handleMarkAsRead = () => {
this.props.markRead(); this.props.markRead();
} };
handleReply = () => { handleReply = () => {
this.props.reply(this.props.lastStatus, this.context.router.history); this.props.reply(this.props.lastStatus, this.context.router.history);
} };
handleDelete = () => { handleDelete = () => {
this.props.delete(); this.props.delete();
} };
handleHotkeyMoveUp = () => { handleHotkeyMoveUp = () => {
this.props.onMoveUp(this.props.conversationId); this.props.onMoveUp(this.props.conversationId);
} };
handleHotkeyMoveDown = () => { handleHotkeyMoveDown = () => {
this.props.onMoveDown(this.props.conversationId); this.props.onMoveDown(this.props.conversationId);
} };
handleConversationMute = () => { handleConversationMute = () => {
this.props.onMute(this.props.lastStatus); this.props.onMute(this.props.lastStatus);
} };
handleShowMore = () => { handleShowMore = () => {
this.props.onToggleHidden(this.props.lastStatus); this.props.onToggleHidden(this.props.lastStatus);
} };
setNamesRef = (c) => { setNamesRef = (c) => {
this.namesNode = c; this.namesNode = c;
} };
render () { render() {
const { accounts, lastStatus, unread, intl } = this.props; const { accounts, lastStatus, unread, intl } = this.props;
if (lastStatus === null) { if (lastStatus === null) {
@ -138,7 +138,10 @@ class Conversation extends ImmutablePureComponent {
null, null,
]; ];
menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute }); menu.push({
text : intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation),
action: this.handleConversationMute,
});
if (unread) { if (unread) {
menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead }); menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead });
@ -147,33 +150,60 @@ class Conversation extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
const names = accounts.map(a => <Permalink to={`/accounts/${a.get('id')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]); const names = accounts.map(a => (<Permalink
to={`/accounts/${a.get('id')}`}
href={a.get('url')}
key={a.get('id')}
title={a.get('acct')}
>
<bdi ><strong
className='display-name__html'
dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
/></bdi >
</Permalink >)).reduce((prev, cur) => [prev, ', ', cur]);
const handlers = { const handlers = {
reply: this.handleReply, reply : this.handleReply,
open: this.handleClick, open : this.handleClick,
moveUp: this.handleHotkeyMoveUp, moveUp : this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown, moveDown : this.handleHotkeyMoveDown,
toggleHidden: this.handleShowMore, toggleHidden: this.handleShowMore,
}; };
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'> <div
className={classNames('conversation focusable muted', { 'conversation--unread': unread })}
tabIndex='0'
>
<div className='conversation__avatar'> <div className='conversation__avatar'>
<AvatarComposite accounts={accounts} size={48} /> <AvatarComposite
</div> accounts={accounts}
size={48}
/>
</div >
<div className='conversation__content'> <div className='conversation__content'>
<div className='conversation__content__info'> <div className='conversation__content__info'>
<div className='conversation__content__relative-time'> <div className='conversation__content__relative-time'>
{unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} /> <span className='conversation_created-at'>
</div> {lastStatus.get('created_at').substr(0, 10)}
</span >
{unread && <span className='conversation__unread' />}
<RelativeTimestamp timestamp={lastStatus.get('created_at')} />
</div >
<div className='conversation__content__names' ref={this.setNamesRef}> <div
<FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} /> className='conversation__content__names'
</div> ref={this.setNamesRef}
</div> >
<FormattedMessage
id='conversation.with'
defaultMessage='With {names}'
values={{ names: <span >{names}</span > }}
/>
</div >
</div >
<StatusContent <StatusContent
status={lastStatus} status={lastStatus}
@ -191,15 +221,28 @@ class Conversation extends ImmutablePureComponent {
)} )}
<div className='status__action-bar'> <div className='status__action-bar'>
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReply} />
<div className='status__action-bar-dropdown'> <div className='status__action-bar-dropdown'>
<DropdownMenuContainer status={lastStatus} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} /> <DropdownMenuContainer
</div> status={lastStatus}
</div> items={menu}
</div> icon='ellipsis-h'
</div> direction='right'
</HotKeys> style={{ width: '15em' }}
size={18}
title={intl.formatMessage(messages.more)}
/>
</div >
<IconButton
className='status__action-bar-button conversation_reply'
title={intl.formatMessage(messages.reply)}
icon='reply'
size='15em'
onClick={this.handleReply}
/>
</div >
</div >
</div >
</HotKeys >
); );
} }

View File

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import { fetchReblogs } from '../../actions/interactions'; import { fetchReblogs } from '../../actions/interactions';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
@ -25,15 +25,15 @@ export default @connect(mapStateToProps)
class Reblogs extends ImmutablePureComponent { class Reblogs extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params : PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch : PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func, shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list, accountIds : ImmutablePropTypes.list,
multiColumn: PropTypes.bool, multiColumn : PropTypes.bool,
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
}; };
componentWillMount () { componentWillMount() {
if (!this.props.accountIds) { if (!this.props.accountIds) {
this.props.dispatch(fetchReblogs(this.props.params.statusId)); this.props.dispatch(fetchReblogs(this.props.params.statusId));
} }
@ -47,20 +47,23 @@ class Reblogs extends ImmutablePureComponent {
handleRefresh = () => { handleRefresh = () => {
this.props.dispatch(fetchReblogs(this.props.params.statusId)); this.props.dispatch(fetchReblogs(this.props.params.statusId));
} };
render () { render() {
const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props; const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props;
if (!accountIds) { if (!accountIds) {
return ( return (
<Column> <Column >
<LoadingIndicator /> <LoadingIndicator />
</Column> </Column >
); );
} }
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has boosted this toot yet. When someone does, they will show up here.' />; const emptyMessage = (<FormattedMessage
id='status.reblogs.empty'
defaultMessage='No one has boosted this toot yet. When someone does, they will show up here.'
/>);
return ( return (
<Column bindToDocument={!multiColumn}> <Column bindToDocument={!multiColumn}>
@ -68,7 +71,12 @@ class Reblogs extends ImmutablePureComponent {
showBackButton showBackButton
multiColumn={multiColumn} multiColumn={multiColumn}
extraButton={( extraButton={(
<button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button> <button
className='column-header__button'
title={intl.formatMessage(messages.refresh)}
aria-label={intl.formatMessage(messages.refresh)}
onClick={this.handleRefresh}
><Icon id='refresh' /></button >
)} )}
/> />
@ -79,10 +87,14 @@ class Reblogs extends ImmutablePureComponent {
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >
{accountIds.map(id => {accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} /> (<AccountContainer
key={id}
id={id}
withNote={false}
/>),
)} )}
</ScrollableList> </ScrollableList >
</Column> </Column >
); );
} }

View File

@ -5,36 +5,36 @@ import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { me, isStaff } from '../../../initial_state'; import { isStaff, me } from '../../../initial_state';
const messages = defineMessages({ const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' }, delete : { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, redraft : { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, direct : { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mention : { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' }, reply : { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog : { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' }, reblog_private : { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog : { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, favourite : { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, bookmark : { id: 'status.bookmark', defaultMessage: 'Bookmark' },
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' }, mute : { id: 'status.mute', defaultMessage: 'Mute @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, muteConversation : { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, unmuteConversation : { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
block: { id: 'status.block', defaultMessage: 'Block @{name}' }, block : { id: 'status.block', defaultMessage: 'Block @{name}' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' }, report : { id: 'status.report', defaultMessage: 'Report @{name}' },
share: { id: 'status.share', defaultMessage: 'Share' }, share : { id: 'status.share', defaultMessage: 'Share' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, pin : { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, unpin : { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' }, embed : { id: 'status.embed', defaultMessage: 'Embed' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_account : { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, admin_status : { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, copy : { id: 'status.copy', defaultMessage: 'Copy link to status' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, blockDomain : { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, unblockDomain : { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unmute : { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unblock : { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
}); });
const mapStateToProps = (state, { status }) => ({ const mapStateToProps = (state, { status }) => ({
@ -50,59 +50,59 @@ class ActionBar extends React.PureComponent {
}; };
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status : ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map, relationship : ImmutablePropTypes.map,
onReply: PropTypes.func.isRequired, onReply : PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired, onReblog : PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired, onFavourite : PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired, onBookmark : PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete : PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired, onDirect : PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention : PropTypes.func.isRequired,
onMute: PropTypes.func, onMute : PropTypes.func,
onUnmute: PropTypes.func, onUnmute : PropTypes.func,
onBlock: PropTypes.func, onBlock : PropTypes.func,
onUnblock: PropTypes.func, onUnblock : PropTypes.func,
onBlockDomain: PropTypes.func, onBlockDomain : PropTypes.func,
onUnblockDomain: PropTypes.func, onUnblockDomain : PropTypes.func,
onMuteConversation: PropTypes.func, onMuteConversation: PropTypes.func,
onReport: PropTypes.func, onReport : PropTypes.func,
onPin: PropTypes.func, onPin : PropTypes.func,
onEmbed: PropTypes.func, onEmbed : PropTypes.func,
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
}; };
handleReplyClick = () => { handleReplyClick = () => {
this.props.onReply(this.props.status); this.props.onReply(this.props.status);
} };
handleReblogClick = (e) => { handleReblogClick = (e) => {
this.props.onReblog(this.props.status, e); this.props.onReblog(this.props.status, e);
} };
handleFavouriteClick = () => { handleFavouriteClick = () => {
this.props.onFavourite(this.props.status); this.props.onFavourite(this.props.status);
} };
handleBookmarkClick = (e) => { handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e); this.props.onBookmark(this.props.status, e);
} };
handleDeleteClick = () => { handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history); this.props.onDelete(this.props.status, this.context.router.history);
} };
handleRedraftClick = () => { handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.context.router.history, true); this.props.onDelete(this.props.status, this.context.router.history, true);
} };
handleDirectClick = () => { handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history); this.props.onDirect(this.props.status.get('account'), this.context.router.history);
} };
handleMentionClick = () => { handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history); this.props.onMention(this.props.status.get('account'), this.context.router.history);
} };
handleMuteClick = () => { handleMuteClick = () => {
const { status, relationship, onMute, onUnmute } = this.props; const { status, relationship, onMute, onUnmute } = this.props;
@ -113,7 +113,7 @@ class ActionBar extends React.PureComponent {
} else { } else {
onMute(account); onMute(account);
} }
} };
handleBlockClick = () => { handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props; const { status, relationship, onBlock, onUnblock } = this.props;
@ -124,44 +124,44 @@ class ActionBar extends React.PureComponent {
} else { } else {
onBlock(status); onBlock(status);
} }
} };
handleBlockDomain = () => { handleBlockDomain = () => {
const { status, onBlockDomain } = this.props; const { status, onBlockDomain } = this.props;
const account = status.get('account'); const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]); onBlockDomain(account.get('acct').split('@')[1]);
} };
handleUnblockDomain = () => { handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props; const { status, onUnblockDomain } = this.props;
const account = status.get('account'); const account = status.get('account');
onUnblockDomain(account.get('acct').split('@')[1]); onUnblockDomain(account.get('acct').split('@')[1]);
} };
handleConversationMuteClick = () => { handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status); this.props.onMuteConversation(this.props.status);
} };
handleReport = () => { handleReport = () => {
this.props.onReport(this.props.status); this.props.onReport(this.props.status);
} };
handlePinClick = () => { handlePinClick = () => {
this.props.onPin(this.props.status); this.props.onPin(this.props.status);
} };
handleShare = () => { handleShare = () => {
navigator.share({ navigator.share({
text: this.props.status.get('search_index'), text: this.props.status.get('search_index'),
url: this.props.status.get('url'), url : this.props.status.get('url'),
}); });
} };
handleEmbed = () => { handleEmbed = () => {
this.props.onEmbed(this.props.status); this.props.onEmbed(this.props.status);
} };
handleCopy = () => { handleCopy = () => {
const url = this.props.status.get('url'); const url = this.props.status.get('url');
@ -180,9 +180,9 @@ class ActionBar extends React.PureComponent {
} finally { } finally {
document.body.removeChild(textarea); document.body.removeChild(textarea);
} }
} };
render () { render() {
const { status, relationship, intl } = this.props; const { status, relationship, intl } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
@ -199,36 +199,66 @@ class ActionBar extends React.PureComponent {
if (me === status.getIn(['account', 'id'])) { if (me === status.getIn(['account', 'id'])) {
if (publicStatus) { if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); menu.push({
text : intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin),
action: this.handlePinClick,
});
} else { } else {
if (status.get('visibility') === 'private') { if (status.get('visibility') === 'private') {
menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick }); menu.push({
text : intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private),
action: this.handleReblogClick,
});
} }
} }
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); menu.push({
text : intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
action: this.handleConversationMuteClick,
});
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else { } else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); text : intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }),
action: this.handleMentionClick,
});
menu.push({
text : intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }),
action: this.handleDirectClick,
});
menu.push(null); menu.push(null);
if (relationship && relationship.get('muting')) { if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); menu.push({
text : intl.formatMessage(messages.unmute, { name: account.get('username') }),
action: this.handleMuteClick,
});
} else { } else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick }); menu.push({
text : intl.formatMessage(messages.mute, { name: account.get('username') }),
action: this.handleMuteClick,
});
} }
if (relationship && relationship.get('blocking')) { if (relationship && relationship.get('blocking')) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick }); menu.push({
text : intl.formatMessage(messages.unblock, { name: account.get('username') }),
action: this.handleBlockClick,
});
} else { } else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick }); menu.push({
text : intl.formatMessage(messages.block, { name: account.get('username') }),
action: this.handleBlockClick,
});
} }
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); menu.push({
text : intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }),
action: this.handleReport,
});
if (account.get('acct') !== account.get('username')) { if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1]; const domain = account.get('acct').split('@')[1];
@ -244,13 +274,23 @@ class ActionBar extends React.PureComponent {
if (isStaff) { if (isStaff) {
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); menu.push({
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }),
href: `/admin/accounts/${status.getIn(['account', 'id'])}`,
});
menu.push({
text: intl.formatMessage(messages.admin_status),
href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}`,
});
} }
} }
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div> <div className='detailed-status__button'><IconButton
title={intl.formatMessage(messages.share)}
icon='share-alt'
onClick={this.handleShare}
/></div >
); );
let replyIcon; let replyIcon;
@ -268,16 +308,48 @@ class ActionBar extends React.PureComponent {
return ( return (
<div className='detailed-status__action-bar'> <div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div> <div className='detailed-status__button'><IconButton
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> title={intl.formatMessage(messages.reply)}
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div> icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon}
onClick={this.handleReplyClick}
/></div >
<div className='detailed-status__button'><IconButton
disabled={reblog_disabled}
active={status.get('reblogged')}
title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)}
icon={reblogIcon}
onClick={this.handleReblogClick}
/></div >
<div className='detailed-status__button'>
<IconButton
className='star-icon'
animate
active={status.get('favourited')}
title={intl.formatMessage(messages.favourite)}
icon='star'
onClick={this.handleFavouriteClick}
/></div >
{shareButton} {shareButton}
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__button'>
<IconButton
className='bookmark-icon'
active={status.get('bookmarked')}
title={intl.formatMessage(messages.bookmark)}
icon='bookmark'
onClick={this.handleBookmarkClick}
/></div >
<div className='detailed-status__action-bar-dropdown'> <div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title='More' /> <DropdownMenuContainer
</div> size={18}
</div> icon='ellipsis-h'
status={status}
items={menu}
direction='left'
title='More'
/>
</div >
</div >
); );
} }

View File

@ -5,41 +5,24 @@ import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchStatus } from '../../actions/statuses'; import { deleteStatus, fetchStatus, hideStatus, muteStatus, revealStatus, unmuteStatus } from '../../actions/statuses';
import MissingIndicator from '../../components/missing_indicator'; import MissingIndicator from '../../components/missing_indicator';
import DetailedStatus from './components/detailed_status'; import DetailedStatus from './components/detailed_status';
import ActionBar from './components/action_bar'; import ActionBar from './components/action_bar';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import { import {
favourite,
unfavourite,
bookmark, bookmark,
unbookmark, favourite,
reblog,
unreblog,
pin, pin,
reblog,
unbookmark,
unfavourite,
unpin, unpin,
unreblog,
} from '../../actions/interactions'; } from '../../actions/interactions';
import { import { directCompose, mentionCompose, replyCompose } from '../../actions/compose';
replyCompose, import { unblockAccount, unmuteAccount } from '../../actions/accounts';
mentionCompose, import { blockDomain, unblockDomain } from '../../actions/domain_blocks';
directCompose,
} from '../../actions/compose';
import {
muteStatus,
unmuteStatus,
deleteStatus,
hideStatus,
revealStatus,
} from '../../actions/statuses';
import {
unblockAccount,
unmuteAccount,
} from '../../actions/accounts';
import {
blockDomain,
unblockDomain,
} from '../../actions/domain_blocks';
import { initMuteModal } from '../../actions/mutes'; import { initMuteModal } from '../../actions/mutes';
import { initBlockModal } from '../../actions/blocks'; import { initBlockModal } from '../../actions/blocks';
import { initReport } from '../../actions/reports'; import { initReport } from '../../actions/reports';
@ -49,24 +32,33 @@ import ColumnBackButton from '../../components/column_back_button';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
import StatusContainer from '../../containers/status_container'; import StatusContainer from '../../containers/status_container';
import { openModal } from '../../actions/modal'; import { openModal } from '../../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state'; import { boostModal, deleteModal } from '../../initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; import { defaultMediaVisibility, textForScreenReader } from '../../components/status';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm : { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, deleteMessage : {
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, id : 'confirmations.delete.message',
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, defaultMessage: 'Are you sure you want to delete this status?',
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, redraftConfirm : { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, redraftMessage : {
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, id : 'confirmations.redraft.message',
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.',
},
revealAll : { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll : { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
detailedStatus : { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm : { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage : {
id : 'confirmations.reply.message',
defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?',
},
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
}); });
@ -116,7 +108,8 @@ const makeMapStateToProps = () => {
let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account')); let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
if (insertAt !== -1) { if (insertAt !== -1) {
descendantsIds.forEach((id, idx) => { descendantsIds.forEach((id, idx) => {
if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) { let account = statuses.get(id).get('account');
if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === account) {
descendantsIds.splice(idx, 1); descendantsIds.splice(idx, 1);
descendantsIds.splice(insertAt, 0, id); descendantsIds.splice(insertAt, 0, id);
insertAt += 1; insertAt += 1;
@ -142,7 +135,7 @@ const makeMapStateToProps = () => {
ancestorsIds, ancestorsIds,
descendantsIds, descendantsIds,
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
domain: state.getIn(['meta', 'domain']), domain : state.getIn(['meta', 'domain']),
}; };
}; };
@ -158,45 +151,48 @@ class Status extends ImmutablePureComponent {
}; };
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params : PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch : PropTypes.func.isRequired,
status: ImmutablePropTypes.map, status : ImmutablePropTypes.map,
ancestorsIds: ImmutablePropTypes.list, ancestorsIds : ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list, descendantsIds : ImmutablePropTypes.list,
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool, askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn : PropTypes.bool,
domain: PropTypes.string.isRequired, domain : PropTypes.string.isRequired,
}; };
state = { state = {
fullscreen: false, fullscreen : false,
showMedia: defaultMediaVisibility(this.props.status), showMedia : defaultMediaVisibility(this.props.status),
loadedStatusId: undefined, loadedStatusId: undefined,
}; };
componentWillMount () { componentWillMount() {
this.props.dispatch(fetchStatus(this.props.params.statusId)); this.props.dispatch(fetchStatus(this.props.params.statusId));
} }
componentDidMount () { componentDidMount() {
attachFullscreenListener(this.onFullScreenChange); attachFullscreenListener(this.onFullScreenChange);
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this._scrolledIntoView = false; this._scrolledIntoView = false;
this.props.dispatch(fetchStatus(nextProps.params.statusId)); this.props.dispatch(fetchStatus(nextProps.params.statusId));
} }
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) { if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') }); this.setState({
showMedia : defaultMediaVisibility(nextProps.status),
loadedStatusId: nextProps.status.get('id'),
});
} }
} }
handleToggleMediaVisibility = () => { handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia }); this.setState({ showMedia: !this.state.showMedia });
} };
handleFavouriteClick = (status) => { handleFavouriteClick = (status) => {
if (status.get('favourited')) { if (status.get('favourited')) {
@ -204,7 +200,7 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(favourite(status)); this.props.dispatch(favourite(status));
} }
} };
handlePin = (status) => { handlePin = (status) => {
if (status.get('pinned')) { if (status.get('pinned')) {
@ -212,24 +208,24 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(pin(status)); this.props.dispatch(pin(status));
} }
} };
handleReplyClick = (status) => { handleReplyClick = (status) => {
let { askReplyConfirmation, dispatch, intl } = this.props; let { askReplyConfirmation, dispatch, intl } = this.props;
if (askReplyConfirmation) { if (askReplyConfirmation) {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.replyMessage), message : intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm), confirm : intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), onConfirm: () => dispatch(replyCompose(status, this.context.router.history)),
})); }));
} else { } else {
dispatch(replyCompose(status, this.context.router.history)); dispatch(replyCompose(status, this.context.router.history));
} }
} };
handleModalReblog = (status) => { handleModalReblog = (status) => {
this.props.dispatch(reblog(status)); this.props.dispatch(reblog(status));
} };
handleReblogClick = (status, e) => { handleReblogClick = (status, e) => {
if (status.get('reblogged')) { if (status.get('reblogged')) {
@ -241,7 +237,7 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog })); this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
} }
} }
} };
handleBookmarkClick = (status) => { handleBookmarkClick = (status) => {
if (status.get('bookmarked')) { if (status.get('bookmarked')) {
@ -249,7 +245,7 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(bookmark(status)); this.props.dispatch(bookmark(status));
} }
} };
handleDeleteClick = (status, history, withRedraft = false) => { handleDeleteClick = (status, history, withRedraft = false) => {
const { dispatch, intl } = this.props; const { dispatch, intl } = this.props;
@ -258,28 +254,28 @@ class Status extends ImmutablePureComponent {
dispatch(deleteStatus(status.get('id'), history, withRedraft)); dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), message : intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), confirm : intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
})); }));
} }
} };
handleDirectClick = (account, router) => { handleDirectClick = (account, router) => {
this.props.dispatch(directCompose(account, router)); this.props.dispatch(directCompose(account, router));
} };
handleMentionClick = (account, router) => { handleMentionClick = (account, router) => {
this.props.dispatch(mentionCompose(account, router)); this.props.dispatch(mentionCompose(account, router));
} };
handleOpenMedia = (media, index) => { handleOpenMedia = (media, index) => {
this.props.dispatch(openModal('MEDIA', { media, index })); this.props.dispatch(openModal('MEDIA', { media, index }));
} };
handleOpenVideo = (media, time) => { handleOpenVideo = (media, time) => {
this.props.dispatch(openModal('VIDEO', { media, time })); this.props.dispatch(openModal('VIDEO', { media, time }));
} };
handleHotkeyOpenMedia = e => { handleHotkeyOpenMedia = e => {
const status = this._properStatus(); const status = this._properStatus();
@ -295,11 +291,11 @@ class Status extends ImmutablePureComponent {
this.handleOpenMedia(status.get('media_attachments'), 0); this.handleOpenMedia(status.get('media_attachments'), 0);
} }
} }
} };
handleMuteClick = (account) => { handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account)); this.props.dispatch(initMuteModal(account));
} };
handleConversationMuteClick = (status) => { handleConversationMuteClick = (status) => {
if (status.get('muted')) { if (status.get('muted')) {
@ -307,7 +303,7 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(muteStatus(status.get('id'))); this.props.dispatch(muteStatus(status.get('id')));
} }
} };
handleToggleHidden = (status) => { handleToggleHidden = (status) => {
if (status.get('hidden')) { if (status.get('hidden')) {
@ -315,7 +311,7 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(hideStatus(status.get('id'))); this.props.dispatch(hideStatus(status.get('id')));
} }
} };
handleToggleAll = () => { handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
@ -326,80 +322,83 @@ class Status extends ImmutablePureComponent {
} else { } else {
this.props.dispatch(hideStatus(statusIds)); this.props.dispatch(hideStatus(statusIds));
} }
} };
handleBlockClick = (status) => { handleBlockClick = (status) => {
const { dispatch } = this.props; const { dispatch } = this.props;
const account = status.get('account'); const account = status.get('account');
dispatch(initBlockModal(account)); dispatch(initBlockModal(account));
} };
handleReport = (status) => { handleReport = (status) => {
this.props.dispatch(initReport(status.get('account'), status)); this.props.dispatch(initReport(status.get('account'), status));
} };
handleEmbed = (status) => { handleEmbed = (status) => {
this.props.dispatch(openModal('EMBED', { url: status.get('url') })); this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
} };
handleUnmuteClick = account => { handleUnmuteClick = account => {
this.props.dispatch(unmuteAccount(account.get('id'))); this.props.dispatch(unmuteAccount(account.get('id')));
} };
handleUnblockClick = account => { handleUnblockClick = account => {
this.props.dispatch(unblockAccount(account.get('id'))); this.props.dispatch(unblockAccount(account.get('id')));
} };
handleBlockDomainClick = domain => { handleBlockDomainClick = domain => {
this.props.dispatch(openModal('CONFIRM', { this.props.dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />, message : <FormattedMessage
confirm: this.props.intl.formatMessage(messages.blockDomainConfirm), id='confirmations.domain_block.message'
defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.'
values={{ domain: <strong >{domain}</strong > }}
/>,
confirm : this.props.intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => this.props.dispatch(blockDomain(domain)), onConfirm: () => this.props.dispatch(blockDomain(domain)),
})); }));
} };
handleUnblockDomainClick = domain => { handleUnblockDomainClick = domain => {
this.props.dispatch(unblockDomain(domain)); this.props.dispatch(unblockDomain(domain));
} };
handleHotkeyMoveUp = () => { handleHotkeyMoveUp = () => {
this.handleMoveUp(this.props.status.get('id')); this.handleMoveUp(this.props.status.get('id'));
} };
handleHotkeyMoveDown = () => { handleHotkeyMoveDown = () => {
this.handleMoveDown(this.props.status.get('id')); this.handleMoveDown(this.props.status.get('id'));
} };
handleHotkeyReply = e => { handleHotkeyReply = e => {
e.preventDefault(); e.preventDefault();
this.handleReplyClick(this.props.status); this.handleReplyClick(this.props.status);
} };
handleHotkeyFavourite = () => { handleHotkeyFavourite = () => {
this.handleFavouriteClick(this.props.status); this.handleFavouriteClick(this.props.status);
} };
handleHotkeyBoost = () => { handleHotkeyBoost = () => {
this.handleReblogClick(this.props.status); this.handleReblogClick(this.props.status);
} };
handleHotkeyMention = e => { handleHotkeyMention = e => {
e.preventDefault(); e.preventDefault();
this.handleMentionClick(this.props.status.get('account')); this.handleMentionClick(this.props.status.get('account'));
} };
handleHotkeyOpenProfile = () => { handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
} };
handleHotkeyToggleHidden = () => { handleHotkeyToggleHidden = () => {
this.handleToggleHidden(this.props.status); this.handleToggleHidden(this.props.status);
} };
handleHotkeyToggleSensitive = () => { handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility(); this.handleToggleMediaVisibility();
} };
handleMoveUp = id => { handleMoveUp = id => {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
@ -416,7 +415,7 @@ class Status extends ImmutablePureComponent {
this._selectChild(index - 1, true); this._selectChild(index - 1, true);
} }
} }
} };
handleMoveDown = id => { handleMoveDown = id => {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
@ -433,9 +432,9 @@ class Status extends ImmutablePureComponent {
this._selectChild(index + 1, false); this._selectChild(index + 1, false);
} }
} }
} };
_selectChild (index, align_top) { _selectChild(index, align_top) {
const container = this.node; const container = this.node;
const element = container.querySelectorAll('.focusable')[index]; const element = container.querySelectorAll('.focusable')[index];
@ -449,7 +448,7 @@ class Status extends ImmutablePureComponent {
} }
} }
renderChildren (list) { renderChildren(list) {
return list.map(id => ( return list.map(id => (
<StatusContainer <StatusContainer
key={id} key={id}
@ -463,9 +462,9 @@ class Status extends ImmutablePureComponent {
setRef = c => { setRef = c => {
this.node = c; this.node = c;
} };
componentDidUpdate () { componentDidUpdate() {
if (this._scrolledIntoView) { if (this._scrolledIntoView) {
return; return;
} }
@ -482,65 +481,85 @@ class Status extends ImmutablePureComponent {
} }
} }
componentWillUnmount () { componentWillUnmount() {
detachFullscreenListener(this.onFullScreenChange); detachFullscreenListener(this.onFullScreenChange);
} }
onFullScreenChange = () => { onFullScreenChange = () => {
this.setState({ fullscreen: isFullscreen() }); this.setState({ fullscreen: isFullscreen() });
} };
render () { render() {
let ancestors, descendants; let ancestors, descendants;
const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props; const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props;
const { fullscreen } = this.state; const { fullscreen } = this.state;
if (status === null) { if (status === null) {
return ( return (
<Column> <Column >
<ColumnBackButton multiColumn={multiColumn} /> <ColumnBackButton multiColumn={multiColumn} />
<MissingIndicator /> <MissingIndicator />
</Column> </Column >
); );
} }
if (ancestorsIds && ancestorsIds.size > 0) { if (ancestorsIds && ancestorsIds.size > 0) {
ancestors = <div>{this.renderChildren(ancestorsIds)}</div>; ancestors = <div >{this.renderChildren(ancestorsIds)}</div >;
} }
if (descendantsIds && descendantsIds.size > 0) { if (descendantsIds && descendantsIds.size > 0) {
descendants = <div>{this.renderChildren(descendantsIds)}</div>; descendants = <div >{this.renderChildren(descendantsIds)}</div >;
} }
const handlers = { const handlers = {
moveUp: this.handleHotkeyMoveUp, moveUp : this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown, moveDown : this.handleHotkeyMoveDown,
reply: this.handleHotkeyReply, reply : this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite, favourite : this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost, boost : this.handleHotkeyBoost,
mention: this.handleHotkeyMention, mention : this.handleHotkeyMention,
openProfile: this.handleHotkeyOpenProfile, openProfile : this.handleHotkeyOpenProfile,
toggleHidden: this.handleHotkeyToggleHidden, toggleHidden : this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive, toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia, openMedia : this.handleHotkeyOpenMedia,
}; };
return ( return (
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.detailedStatus)}> <Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.detailedStatus)}
>
<ColumnHeader <ColumnHeader
showBackButton showBackButton
multiColumn={multiColumn} multiColumn={multiColumn}
extraButton={( extraButton={(
<button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button> <button
className='column-header__button'
title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)}
aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)}
onClick={this.handleToggleAll}
aria-pressed={status.get('hidden') ? 'false' : 'true'}
><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button >
)} )}
/> />
<ScrollContainer scrollKey='thread' shouldUpdateScroll={shouldUpdateScroll}> <ScrollContainer
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}> scrollKey='thread'
shouldUpdateScroll={shouldUpdateScroll}
>
<div
className={classNames('scrollable', { fullscreen })}
ref={this.setRef}
>
{/*<h2 className='debug'>ancestors:</h2 >*/}
{ancestors} {ancestors}
{/*<h2 className='debug'>common:</h2 >*/}
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('focusable', 'detailed-status__wrapper')} tabIndex='0' aria-label={textForScreenReader(intl, status, false)}> <div
className={classNames('focusable', 'detailed-status__wrapper')}
tabIndex='0'
aria-label={textForScreenReader(intl, status, false)}
>
<DetailedStatus <DetailedStatus
status={status} status={status}
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
@ -571,13 +590,13 @@ class Status extends ImmutablePureComponent {
onPin={this.handlePin} onPin={this.handlePin}
onEmbed={this.handleEmbed} onEmbed={this.handleEmbed}
/> />
</div> </div >
</HotKeys> </HotKeys >
{/*<h2 className='debug'>Descendants:</h2 >*/}
{descendants} {descendants}
</div> </div >
</ScrollContainer> </ScrollContainer >
</Column> </Column >
); );
} }

View File

@ -5,7 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import ReactSwipeableViews from 'react-swipeable-views'; import ReactSwipeableViews from 'react-swipeable-views';
import TabsBar, { links, getIndex, getLink } from './tabs_bar'; import TabsBar, { getIndex, getLink, links } from './tabs_bar';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import BundleContainer from '../containers/bundle_container'; import BundleContainer from '../containers/bundle_container';
@ -13,17 +13,17 @@ import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading'; import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error'; import BundleColumnError from './bundle_column_error';
import { import {
Compose, BookmarkedStatuses,
Notifications,
HomeTimeline,
CommunityTimeline, CommunityTimeline,
PublicTimeline, Compose,
HashtagTimeline, Directory,
DirectTimeline, DirectTimeline,
FavouritedStatuses, FavouritedStatuses,
BookmarkedStatuses, HashtagTimeline,
HomeTimeline,
ListTimeline, ListTimeline,
Directory, Notifications,
PublicTimeline,
} from '../../ui/util/async-components'; } from '../../ui/util/async-components';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import ComposePanel from './compose_panel'; import ComposePanel from './compose_panel';
@ -31,19 +31,20 @@ import NavigationPanel from './navigation_panel';
import detectPassiveEvents from 'detect-passive-events'; import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from '../../../scroll'; import { scrollRight } from '../../../scroll';
import LinkFooter from './link_footer';
const componentMap = { const componentMap = {
'COMPOSE': Compose, 'COMPOSE' : Compose,
'HOME': HomeTimeline, 'HOME' : HomeTimeline,
'NOTIFICATIONS': Notifications, 'NOTIFICATIONS': Notifications,
'PUBLIC': PublicTimeline, 'PUBLIC' : PublicTimeline,
'COMMUNITY': CommunityTimeline, 'COMMUNITY' : CommunityTimeline,
'HASHTAG': HashtagTimeline, 'HASHTAG' : HashtagTimeline,
'DIRECT': DirectTimeline, 'DIRECT' : DirectTimeline,
'FAVOURITES': FavouritedStatuses, 'FAVOURITES' : FavouritedStatuses,
'BOOKMARKS': BookmarkedStatuses, 'BOOKMARKS' : BookmarkedStatuses,
'LIST': ListTimeline, 'LIST' : ListTimeline,
'DIRECTORY': Directory, 'DIRECTORY' : Directory,
}; };
const messages = defineMessages({ const messages = defineMessages({
@ -60,16 +61,16 @@ class ColumnsArea extends ImmutablePureComponent {
}; };
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
columns: ImmutablePropTypes.list.isRequired, columns : ImmutablePropTypes.list.isRequired,
isModalOpen: PropTypes.bool.isRequired, isModalOpen : PropTypes.bool.isRequired,
singleColumn: PropTypes.bool, singleColumn: PropTypes.bool,
children: PropTypes.node, children : PropTypes.node,
}; };
state = { state = {
shouldAnimate: false, shouldAnimate: false,
} };
componentWillReceiveProps() { componentWillReceiveProps() {
this.setState({ shouldAnimate: false }); this.setState({ shouldAnimate: false });
@ -100,7 +101,7 @@ class ColumnsArea extends ImmutablePureComponent {
this.setState({ shouldAnimate: true }); this.setState({ shouldAnimate: true });
} }
componentWillUnmount () { componentWillUnmount() {
if (!this.props.singleColumn) { if (!this.props.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel); this.node.removeEventListener('wheel', this.handleWheel);
} }
@ -129,14 +130,14 @@ class ColumnsArea extends ImmutablePureComponent {
this.context.router.history.push(getLink(this.pendingIndex)); this.context.router.history.push(getLink(this.pendingIndex));
this.pendingIndex = null; this.pendingIndex = null;
} }
} };
handleAnimationEnd = () => { handleAnimationEnd = () => {
if (typeof this.pendingIndex === 'number') { if (typeof this.pendingIndex === 'number') {
this.context.router.history.push(getLink(this.pendingIndex)); this.context.router.history.push(getLink(this.pendingIndex));
this.pendingIndex = null; this.pendingIndex = null;
} }
} };
handleWheel = () => { handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') { if (typeof this._interruptScrollAnimation !== 'function') {
@ -144,11 +145,11 @@ class ColumnsArea extends ImmutablePureComponent {
} }
this._interruptScrollAnimation(); this._interruptScrollAnimation();
} };
setRef = (node) => { setRef = (node) => {
this.node = node; this.node = node;
} };
renderView = (link, index) => { renderView = (link, index) => {
const columnIndex = getIndex(this.context.router.history.location.pathname); const columnIndex = getIndex(this.context.router.history.location.pathname);
@ -157,80 +158,119 @@ class ColumnsArea extends ImmutablePureComponent {
const view = (index === columnIndex) ? const view = (index === columnIndex) ?
React.cloneElement(this.props.children) : React.cloneElement(this.props.children) :
<ColumnLoading title={title} icon={icon} />; (<ColumnLoading
title={title}
icon={icon}
/>);
return ( return (
<div className='columns-area columns-area--mobile' key={index}> <div
className='columns-area columns-area--mobile'
key={index}
>
{view} {view}
</div> </div >
); );
} };
renderLoading = columnId => () => { renderLoading = columnId => () => {
return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading />; return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading />;
} };
renderError = (props) => { renderError = (props) => {
return <BundleColumnError {...props} />; return <BundleColumnError {...props} />;
} };
render () { render() {
const { columns, children, singleColumn, isModalOpen, intl } = this.props; const { columns, children, singleColumn, isModalOpen, intl } = this.props;
const { shouldAnimate } = this.state; const { shouldAnimate } = this.state;
const columnIndex = getIndex(this.context.router.history.location.pathname); const columnIndex = getIndex(this.context.router.history.location.pathname);
if (singleColumn) { if (singleColumn) {
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : (<Link
key='floating-action-button'
to='/statuses/new'
className='floating-action-button'
aria-label={intl.formatMessage(messages.publish)}
><Icon id='pencil' /></Link >);
const content = columnIndex !== -1 ? ( const content = columnIndex !== -1 ? (
<ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}> <ReactSwipeableViews
key='content'
hysteresis={0.2}
threshold={15}
index={columnIndex}
onChangeIndex={this.handleSwipe}
onTransitionEnd={this.handleAnimationEnd}
animateTransitions={shouldAnimate}
springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }}
style={{ height: '100%' }}
>
{links.map(this.renderView)} {links.map(this.renderView)}
</ReactSwipeableViews> </ReactSwipeableViews >
) : ( ) : (
<div key='content' className='columns-area columns-area--mobile'>{children}</div> <div
key='content'
className='columns-area columns-area--mobile'
>{children}</div >
); );
return ( return (
<div className='columns-area__panels'> <div className='columns-area__panels'>
<LinkFooter withHotkeys />
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'> <div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'> <div className='columns-area__panels__pane__inner'>
<ComposePanel /> <ComposePanel />
</div> </div >
</div> </div >
<div className='columns-area__panels__main'> <div className='columns-area__panels__main'>
<TabsBar key='tabs' /> <TabsBar key='tabs' />
{content} {content}
</div> </div >
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'> <div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
<div className='columns-area__panels__pane__inner'> <div className='columns-area__panels__pane__inner'>
<NavigationPanel /> <NavigationPanel />
</div> </div >
</div> </div >
{floatingActionButton} {floatingActionButton}
</div> </div >
); );
} }
return ( return (
<div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}> <div
className={`columns-area ${isModalOpen ? 'unscrollable' : ''}`}
ref={this.setRef}
>
{columns.map(column => { {columns.map(column => {
const params = column.get('params', null) === null ? null : column.get('params').toJS(); const params = column.get('params', null) === null ? null : column.get('params').toJS();
const other = params && params.other ? params.other : {}; const other = params && params.other ? params.other : {};
return ( return (
<BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}> <BundleContainer
{SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />} key={column.get('uuid')}
</BundleContainer> fetchComponent={componentMap[column.get('id')]}
loading={this.renderLoading(column.get('id'))}
error={this.renderError}
>
{SpecificComponent => (<SpecificComponent
columnId={column.get('uuid')}
params={params}
multiColumn {...other}
/>)}
</BundleContainer >
); );
})} })}
{React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))} {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
</div> </div >
); )
;
} }
} }

View File

@ -2,15 +2,20 @@ import React from 'react';
import SearchContainer from 'mastodon/features/compose/containers/search_container'; import SearchContainer from 'mastodon/features/compose/containers/search_container';
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container'; import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
import NavigationContainer from 'mastodon/features/compose/containers/navigation_container'; import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
import LinkFooter from './link_footer'; import InstantMessaging from './messaging/instantMessaging';
const showIM = false;
const ComposePanel = () => ( const ComposePanel = () => (
<div className='compose-panel'> <div className='compose-panel'>
<SearchContainer openInRoute /> <SearchContainer openInRoute />
<NavigationContainer /> <NavigationContainer />
<ComposeFormContainer singleColumn /> <ComposeFormContainer singleColumn />
<LinkFooter withHotkeys />
</div> {showIM && (
<InstantMessaging />
)}
</div >
); );
export default ComposePanel; export default ComposePanel;

View File

@ -1,11 +1,12 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state'; import { invitesEnabled, repository, source_url, version } from 'mastodon/initial_state';
import { logOut } from 'mastodon/utils/log_out'; import { logOut } from 'mastodon/utils/log_out';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { isStaff } from '../../../initial_state';
const messages = defineMessages({ const messages = defineMessages({
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
@ -13,23 +14,31 @@ const messages = defineMessages({
}); });
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onLogout () { onLogout() {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage), message : intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm), confirm : intl.formatMessage(messages.logoutConfirm),
onConfirm: () => logOut(), onConfirm: () => logOut(),
})); }));
}, },
}); });
// const themeIsDark = true;
// const displaythemetoggler = true;
export default @injectIntl export default @injectIntl
@connect(null, mapDispatchToProps) @connect(null, mapDispatchToProps)
class LinkFooter extends React.PureComponent { class LinkFooter extends React.PureComponent {
static propTypes = { static propTypes = {
withHotkeys: PropTypes.bool, enableChristmasSnow: PropTypes.bool,
onLogout: PropTypes.func.isRequired, snowActive : PropTypes.bool,
intl: PropTypes.object.isRequired, withHotkeys : PropTypes.bool,
snow : PropTypes.func,
onLogout : PropTypes.func.isRequired,
intl : PropTypes.object.isRequired,
};
static defaultProps = {
enableChristmasSnow: true,
}; };
handleLogoutClick = e => { handleLogoutClick = e => {
@ -39,33 +48,228 @@ class LinkFooter extends React.PureComponent {
this.props.onLogout(); this.props.onLogout();
return false; return false;
};
constructor(props) {
super(props);
Date.prototype.getWeek = function () {
var onejan = new Date(this.getFullYear(), 0, 1);
return Math.ceil((((this - onejan) / 86400000) + onejan.getDay() + 1) / 7);
};
var weekNumber = (new Date()).getWeek();
// display snow during the last two weeks of the year
console.log('week', weekNumber);
const shouldWeDisplaySnow = (weekNumber > 48) && props.enableChristmasSnow;
this.state = { enableChristmasSnow: shouldWeDisplaySnow };
// make snow effect
if (shouldWeDisplaySnow) {
import('../../../utils/snowstorm-min')
.then((snowstorm) => {
Window.snowstorm = snowstorm.default;
this.state.snow = Window.snowstorm;
console.log('this.state.snow ', this.state.snow);
// snowstorm.start();
this.state.snowActive = true;
})
.catch((err) => console.error(err));
} }
render () { }
toggleSnow = () => {
console.log('toggle snow');
if (this.state.snow) {
if (this.state.snowActive) {
this.state.snow.stop();
this.state.snowActive = false;
} else {
this.state.snow.start();
this.state.snowActive = true;
}
}
};
changeTheme(newTheme) {
console.log('change theme en ', newTheme);
}
render() {
const { withHotkeys } = this.props; const { withHotkeys } = this.props;
var snowClasses = this.props.snowActive ? 'snow-button active' : 'snow-button ';
return ( return (
<div className='getting-started__footer'>
<ul>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
{withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
<li><a href='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
</ul>
<p> <div className='getting-started__footer'>
<div className='extras'>
{/*// TODO*/}
{/*<button className='mod-theme btn btn-block'>*/}
{/* {themeIsDark ? (*/}
{/* <span*/}
{/* onClick={this.changeTheme('light')}*/}
{/* title='set light'*/}
{/* >*/}
{/* <i className='fa fa-pencil-o' />*/}
{/* </span >*/}
{/* ) : (*/}
{/* <span*/}
{/* onClick={this.changeTheme('dark')}*/}
{/* title='set dark'*/}
{/* >*/}
{/* <i className='fa fa-pencil' />*/}
{/* </span >*/}
{/* )}*/}
{/*</button >*/}
{this.state.enableChristmasSnow && (
<div
onClick={this.toggleSnow}
className='christmas-snow'
>
<div className={snowClasses}>
<i
className='icon fa fa-snowflake-o'
aria-hidden='true'
/>
</div >
<div > Joyeuses fêtes!</div >
{isStaff && (
<a href='/admin/tags?pending_review=1'>
<i className='fa fa-fire' />
Trending hashtags</a >
)}
<br />
<div className='external-utilities'>
<a href='https://mastodon.cipherbliss.com/@tykayn'>
<i className='fa fa-paper-plane' />
contactez nous
</a >
<a href='https://liberapay.com/cipherbliss'><i className='fa fa-coffee' /> Supportez Cipherbliss</a >
<a href='https://peertube.cipherbliss.com'> <i className='fa fa-play ' /> Videos</a >
<a href='https://framadate.org/'> <i className='fa fa-calendar' /> FramaDate</a >
<a href='https://framapad.org/'> <i className='fa fa-file-text' /> Pad</a >
<a href='https://framagit.org/tykayn/mastodon'> <i className='fa fa-gitlab' /> Source</a >
<hr />
<a href='/web/timelines/tag/vélo'> <i className='fa fa-bicycle' /> #vélo</a >
<a href='/web/timelines/tag/openstreetmap'> <i className='fa fa-map-o' /> #OpenStreetMap</a >
<a href='/web/timelines/tag/mastoart'> <i className='fa fa-paint-brush' /> #Mastoart</a >
<a href='/web/timelines/tag/ironèmes'> <i className='fa fa-file-text-o' /> #ironèmes</a >
</div >
</div >
)}
</div >
<ul >
{invitesEnabled && <li ><a
href='/invites'
target='_blank'
><FormattedMessage
id='getting_started.invite'
defaultMessage='Invite people'
/> ·</a >
</li >}
{withHotkeys && <li ><Link to='/keyboard-shortcuts'>
<FormattedMessage
id='navigation_bar.keyboard_shortcuts'
defaultMessage='Hotkeys'
/> ·
</Link >
</li >}
<li >
<a href='/auth/edit'>
<FormattedMessage
id='getting_started.security'
defaultMessage='Security'
/> ·
</a >
</li >
<li >
<a
href='/about/more'
target='_blank'
><FormattedMessage
id='navigation_bar.info'
defaultMessage='About this server'
/> ·
</a >
</li >
<li >
<a
href='https://joinmastodon.org/apps'
target='_blank'
><FormattedMessage
id='navigation_bar.apps'
defaultMessage='Mobile apps'
/> ·
</a >
</li >
<li >
<a
href='/terms'
target='_blank'
>
<FormattedMessage
id='getting_started.terms'
defaultMessage='Terms of service'
/> ·</a >
</li >
<li >
<a
href='/settings/applications'
target='_blank'
><FormattedMessage
id='getting_started.developers'
defaultMessage='Developers'
/> ·
</a >
</li >
<li >
<a
href='https://docs.joinmastodon.org'
target='_blank'
>
<FormattedMessage
id='getting_started.documentation'
defaultMessage='Documentation'
/>
</a > ·
</li >
<li >
<a
href='/auth/sign_out'
onClick={this.handleLogoutClick}
>
<FormattedMessage
id='navigation_bar.logout'
defaultMessage='Logout'
/>
</a >
</li >
</ul >
<p >
<FormattedMessage <FormattedMessage
id='getting_started.open_source_notice' id='getting_started.open_source_notice'
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.' defaultMessage='Mastodon is open source software. You can contribute or report issues on the forge at {forge}.'
values={{ github: <span><a href={source_url} rel='noopener noreferrer' target='_blank'>{repository}</a> (v{version})</span> }} values={{
forge: <span ><a
href={source_url}
rel='noopener noreferrer'
target='_blank'
>{repository}</a > (v{version})</span >,
}}
/> />
</p> </p >
</div> </div >
); );
} }

View File

@ -0,0 +1,90 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class ContactsList extends React.PureComponent {
static propTypes = {
showList : PropTypes.bool,
contactList : PropTypes.array,
conversationList: PropTypes.array,
};
static defaultProps = {
showList : true,
contactList: ['machin', 'bidule', 'chuck norris'],
};
constructor(props) {
super(props);
this.state = {
showList : true,
contactList : ['machin', 'bidule', 'chuck norris'],
conversationList: ['machin', 'bidule', 'chuck norris'],
};
}
submitCompose() {
console.log('submit message');
}
toggleList = () => {
console.log('toggle');
this.setState((state) => {
console.log('state.showList', state.showList);
return {
showList: !state.showList,
};
});
};
render() {
// return (
// <div >
// liste de contacts
// </div >
// );
const renderedList = this.state.contactList.forEach(elem => {
return (
<li className='contact-item'>
{elem}
</li >
);
});
return (
<div className='messaging-container'>
<div className='messaging-box'>
<div className='title column-header'>
<i
role='img'
className='fa fa-envelope column-header__icon fa-fw'
/>
Messaging box
</div >
<div className='user-list column-header'>
<h2 className='title'>la liste de {this.state.contactList.lengh} contacts
<button
className='btn btn-primary'
onClick={this.toggleList}
>
<i className='fa fa-caret-up' />
</button >
</h2 >
{this.state.showList && (
<div className='contact-list-container'>
<h3 >show list</h3 >
<ul className='contact-list'>
{renderedList}
</ul >
</div >
)}
</div >
</div >
</div >
);
}
}

View File

@ -0,0 +1,67 @@
import React from 'react';
export default class ConversationItem extends React.PureComponent {
following = [];
render() {
const list = (
<li className='conversations_item has-new-message'>
<div className='title'>
<i
role='img'
className='fa fa-envelope column-header__icon fa-fw'
/>
Un Gens
<span className='new-message-counter'>
(3)</span >
<button className='btn-small'>
<i
role='img'
className='fa fa-caret-down column-header__icon fa-fw'
/>
</button >
</div >
<div className='conversation_stream'>
<div className='message theirs'>
<p >oh hello there! 😋 </p >
<div className='arrow-down' />
</div >
<div className='message mine'>
<p >General Emoji</p >
<div className='arrow-down' />
</div >
<div className='message theirs'>
<p >we just achieved comedy</p >
<div className='arrow-down' />
</div >
</div >
<div className='conversation_input'>
<form
action='#'
onSubmit={this.submitCompose()}
>
<textarea
name='messager'
id=''
cols='15'
rows='3'
className='messager-textarea'
placeholder='allez dis nous tout'
/>
<input
type='submit'
name='submit'
value='Send'
/>
</form >
</div >
</li >
);
return list;
}
}

View File

@ -0,0 +1,14 @@
import React from 'react';
export default class ConversationStream extends React.PureComponent {
render() {
return (
<div class='conversation-stream'>
ConversationStream todo
</div >
);
}
}

View File

@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
const following = ['bidule', 'chose', 'truc'];
export default class Conversation extends React.Component {
static propTypes = {
following: PropTypes.array,
// conversations: PropTypes.array,
};
static defaultProps = {
following: following,
};
openConversationWith(name) {
console.log('openConversationWith name', name);
}
render() {
return this.props.following.map(elem =>
(<li className='user-item'>
<div
className='username'
onClick={this.openConversationWith(elem)}
>
Machin {elem}
</div >
<div className='last-active'>3 min</div >
</li >),
);
};
}

View File

@ -0,0 +1,65 @@
import React from 'react';
import ContactsList from './contacts-list';
export default class InstantMessaging extends React.PureComponent {
// static propTypes = {
// following : PropTypes.array,
// conversations: PropTypes.array,
// };
// static defaultProps = {
// threadsCompile: true,
// };
// openConversationWith(account) {
// let conversationFound = account;
// if conversation exist, focus on it
// if (conversationFound) {
//
// } else {
//
// }
// else, create conversation and focus on it
// };
// submitCompose() {
//
// };
// constructor() {
// super();
//
// this.props.conversations = [
// {
// withAccount: '@machin',
// messages : [],
// opened : true,
// },
// {
// withAccount: '@chuck',
// messages : [],
// opened : false,
// },
// ];
// this.props.following = [
// { username: 'wulfila', handle: '@wulfila' },
// { username: 'machin', handle: '@machin' },
// { username: 'chuck norris', handle: '@chuck' },
// ];
//
// }
render() {
return (
<div >
<div className='debug'>
messagerie todo
<ContactsList />
</div >
{/*<Conversation />*/}
</div >
);
}
};

View File

@ -10,27 +10,154 @@ import TrendsContainer from 'mastodon/features/getting_started/containers/trends
const NavigationPanel = () => ( const NavigationPanel = () => (
<div className='navigation-panel'> <div className='navigation-panel'>
<NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink> <div className='small-texts timelines'>
<NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink> <NavLink
className='column-link column-link--transparent'
to='/timelines/home'
data-preview-title-id='column.home'
data-preview-icon='home'
><Icon
className='column-link__icon'
id='home'
fixedWidth
/><FormattedMessage
id='tabs_bar.home'
defaultMessage='Home'
/></NavLink >
<NavLink
className='column-link column-link--transparent'
to='/timelines/public/local'
data-preview-title-id='column.community'
data-preview-icon='users'
><Icon
className='column-link__icon'
id='users'
fixedWidth
/><FormattedMessage
id='tabs_bar.local_timeline'
defaultMessage='Local'
/></NavLink >
<NavLink
className='column-link column-link--transparent'
exact
to='/timelines/public'
data-preview-title-id='column.public'
data-preview-icon='globe'
><Icon
className='column-link__icon'
id='globe'
fixedWidth
/><FormattedMessage
id='tabs_bar.federated_timeline'
defaultMessage='Federated'
/></NavLink >
</div >
<div className='spacer'></div >
<NavLink
className='column-link column-link--transparent'
to='/notifications'
data-preview-title-id='column.notifications'
data-preview-icon='bell'
><NotificationsCounterIcon
className='column-link__icon'
/><FormattedMessage
id='tabs_bar.notifications'
defaultMessage='Notifications'
/></NavLink >
<NavLink
className='column-link column-link--transparent'
to='/timelines/direct'
><Icon
className='column-link__icon'
id='envelope'
fixedWidth
/><FormattedMessage
id='navigation_bar.direct'
defaultMessage='Direct messages'
/></NavLink >
<FollowRequestsNavLink /> <FollowRequestsNavLink />
<NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
<NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> <div className='spacer'></div >
<NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink> <NavLink
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink> className='column-link column-link--transparent'
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink> to='/favourites'
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>} ><Icon
className='column-link__icon'
id='star'
fixedWidth
/><FormattedMessage
id='navigation_bar.favourites'
defaultMessage='Favourites'
/></NavLink >
<NavLink
className='column-link column-link--transparent'
to='/bookmarks'
><Icon
className='column-link__icon'
id='bookmark'
fixedWidth
/><FormattedMessage
id='navigation_bar.bookmarks'
defaultMessage='Bookmarks'
/></NavLink >
<NavLink
className='column-link column-link--transparent'
to='/lists'
><Icon
className='column-link__icon'
id='list-ul'
fixedWidth
/><FormattedMessage
id='navigation_bar.lists'
defaultMessage='Lists'
/></NavLink >
{profile_directory &&
<NavLink
className='column-link column-link--transparent'
to='/directory'
><Icon
className='column-link__icon'
id='address-book-o'
fixedWidth
/><FormattedMessage
id='getting_started.directory'
defaultMessage='Profile directory'
/></NavLink >}
<ListPanel /> <ListPanel />
<hr /> <hr />
<a className='column-link column-link--transparent' href='/settings/preferences'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a> <a
<a className='column-link column-link--transparent' href='/relationships'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a> className='column-link column-link--transparent'
href='/settings/preferences'
><Icon
className='column-link__icon'
id='cog'
fixedWidth
/><FormattedMessage
id='navigation_bar.preferences'
defaultMessage='Preferences'
/></a >
<a
className='column-link column-link--transparent'
href='/relationships'
><Icon
className='column-link__icon'
id='users'
fixedWidth
/><FormattedMessage
id='navigation_bar.follows_and_followers'
defaultMessage='Follows and followers'
/></a >
{showTrends && <div className='flex-spacer' />} {showTrends && <div className='flex-spacer' />}
{showTrends && <TrendsContainer />} {showTrends && <TrendsContainer />}
</div>
</div >
); );
export default withRouter(NavigationPanel); export default withRouter(NavigationPanel);

View File

@ -8,19 +8,81 @@ import Icon from 'mastodon/components/icon';
import NotificationsCounterIcon from './notifications_counter_icon'; import NotificationsCounterIcon from './notifications_counter_icon';
export const links = [ export const links = [
<NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, <NavLink
<NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, className='tabs-bar__link'
<NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, to='/timelines/home'
<NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, data-preview-title-id='column.home'
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>, data-preview-icon='home'
<NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>, ><Icon
id='home'
fixedWidth
/><FormattedMessage
id='tabs_bar.home'
defaultMessage='Home'
/></NavLink >,
<NavLink
className='tabs-bar__link'
to='/notifications'
data-preview-title-id='column.notifications'
data-preview-icon='bell'
><NotificationsCounterIcon /><FormattedMessage
id='tabs_bar.notifications'
defaultMessage='Notifications'
/></NavLink >,
<NavLink
className='tabs-bar__link'
to='/timelines/public/local'
data-preview-title-id='column.community'
data-preview-icon='users'
><Icon
id='users'
fixedWidth
/><FormattedMessage
id='tabs_bar.local_timeline'
defaultMessage='Local'
/></NavLink >,
<NavLink
className='tabs-bar__link'
exact
to='/timelines/public'
data-preview-title-id='column.public'
data-preview-icon='globe'
><Icon
id='globe'
fixedWidth
/><FormattedMessage
id='tabs_bar.federated_timeline'
defaultMessage='Federated'
/></NavLink >,
<NavLink
className='tabs-bar__link optional'
to='/search'
data-preview-title-id='tabs_bar.search'
data-preview-icon='bell'
><Icon
id='search'
fixedWidth
/><FormattedMessage
id='tabs_bar.search'
defaultMessage='Search'
/></NavLink >,
<NavLink
className='tabs-bar__link'
style={{ flexGrow: '0', flexBasis: '30px' }}
to='/getting-started'
data-preview-title-id='getting_started.heading'
data-preview-icon='bars'
><Icon
id='bars'
fixedWidth
/></NavLink >,
]; ];
export function getIndex (path) { export function getIndex(path) {
return links.findIndex(link => link.props.to === path); return links.findIndex(link => link.props.to === path);
} }
export function getLink (index) { export function getLink(index) {
return links[index].props.to; return links[index].props.to;
} }
@ -29,13 +91,13 @@ export default @injectIntl
class TabsBar extends React.PureComponent { class TabsBar extends React.PureComponent {
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
} };
setRef = ref => { setRef = ref => {
this.node = ref; this.node = ref;
} };
handleClick = (e) => { handleClick = (e) => {
// Only apply optimization for touch devices, which we assume are slower // Only apply optimization for touch devices, which we assume are slower
@ -50,7 +112,6 @@ class TabsBar extends React.PureComponent {
const nextTab = tabs.find(tab => tab.contains(e.target)); const nextTab = tabs.find(tab => tab.contains(e.target));
const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)]; const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)];
if (currentTab !== nextTab) { if (currentTab !== nextTab) {
if (currentTab) { if (currentTab) {
currentTab.classList.remove('active'); currentTab.classList.remove('active');
@ -67,19 +128,28 @@ class TabsBar extends React.PureComponent {
}); });
} }
} };
render () { render() {
const { intl: { formatMessage } } = this.props; const { intl: { formatMessage } } = this.props;
return ( return (
<div className='tabs-bar__wrapper'> <div className='tabs-bar__wrapper'>
<nav className='tabs-bar' ref={this.setRef}>
{links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))} <nav
</nav> className='tabs-bar'
ref={this.setRef}
>
{links.map(link => React.cloneElement(link, {
key : link.props.to,
onClick : this.handleClick,
'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }),
}))}
</nav >
<div id='tabs-bar__portal' /> <div id='tabs-bar__portal' />
</div>
</div >
); );
} }

View File

@ -15,9 +15,9 @@ class ReducedMotion extends React.Component {
static propTypes = { static propTypes = {
defaultStyle: PropTypes.object, defaultStyle: PropTypes.object,
style: PropTypes.object, style : PropTypes.object,
children: PropTypes.func, children : PropTypes.func,
} };
render() { render() {
@ -33,9 +33,12 @@ class ReducedMotion extends React.Component {
}); });
return ( return (
<Motion style={style} defaultStyle={defaultStyle}> <Motion
style={style}
defaultStyle={defaultStyle}
>
{children} {children}
</Motion> </Motion >
); );
} }

View File

@ -5,7 +5,7 @@
"account.block_domain": "Tout masquer venant de {domain}", "account.block_domain": "Tout masquer venant de {domain}",
"account.blocked": "Bloqué·e", "account.blocked": "Bloqué·e",
"account.cancel_follow_request": "Annuler la demande de suivi", "account.cancel_follow_request": "Annuler la demande de suivi",
"account.direct": "Envoyer un message direct à @{name}", "account.direct": "Envoyer un message privé à @{name}",
"account.domain_blocked": "Domaine caché", "account.domain_blocked": "Domaine caché",
"account.edit_profile": "Modifier le profil", "account.edit_profile": "Modifier le profil",
"account.endorse": "Recommander sur le profil", "account.endorse": "Recommander sur le profil",
@ -50,7 +50,9 @@
"bundle_modal_error.close": "Fermer", "bundle_modal_error.close": "Fermer",
"bundle_modal_error.message": "Une erreur sest produite lors du chargement de ce composant.", "bundle_modal_error.message": "Une erreur sest produite lors du chargement de ce composant.",
"bundle_modal_error.retry": "Réessayer", "bundle_modal_error.retry": "Réessayer",
"column.bookmarks": "Marque pages",
"column.blocks": "Comptes bloqués", "column.blocks": "Comptes bloqués",
"column.bookmarks": "Bookmarks",
"column.community": "Fil public local", "column.community": "Fil public local",
"column.direct": "Messages privés", "column.direct": "Messages privés",
"column.directory": "Parcourir les profils", "column.directory": "Parcourir les profils",
@ -138,8 +140,9 @@
"empty_column.account_timeline": "Aucun pouet ici !", "empty_column.account_timeline": "Aucun pouet ici !",
"empty_column.account_unavailable": "Profil non disponible", "empty_column.account_unavailable": "Profil non disponible",
"empty_column.blocks": "Vous navez bloqué aucun·e utilisateur·rice pour le moment.", "empty_column.blocks": "Vous navez bloqué aucun·e utilisateur·rice pour le moment.",
"empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
"empty_column.community": "Le fil public local est vide. Écrivez donc quelque chose pour le remplir!", "empty_column.community": "Le fil public local est vide. Écrivez donc quelque chose pour le remplir!",
"empty_column.direct": "Vous navez pas encore de messages directs. Lorsque vous en enverrez ou recevrez un, il saffichera ici.", "empty_column.direct": "Vous navez pas encore de messages privés. Lorsque vous en enverrez ou recevrez un, il saffichera ici.",
"empty_column.domain_blocks": "Il ny a aucun domaine caché pour le moment.", "empty_column.domain_blocks": "Il ny a aucun domaine caché pour le moment.",
"empty_column.favourited_statuses": "Vous navez aucun pouet favoris pour le moment. Lorsque vous en mettrez un en favori, il apparaîtra ici.", "empty_column.favourited_statuses": "Vous navez aucun pouet favoris pour le moment. Lorsque vous en mettrez un en favori, il apparaîtra ici.",
"empty_column.favourites": "Personne na encore mis ce pouet en favori. Lorsque quelquun le fera, il apparaîtra ici.", "empty_column.favourites": "Personne na encore mis ce pouet en favori. Lorsque quelquun le fera, il apparaîtra ici.",
@ -163,7 +166,7 @@
"getting_started.documentation": "Documentation", "getting_started.documentation": "Documentation",
"getting_started.heading": "Pour commencer", "getting_started.heading": "Pour commencer",
"getting_started.invite": "Inviter des gens", "getting_started.invite": "Inviter des gens",
"getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer ou faire des rapports de bogues via {github} sur GitHub.", "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer ou faire des rapports de bogues via {forge} sur GitHub.",
"getting_started.security": "Sécurité", "getting_started.security": "Sécurité",
"getting_started.terms": "Conditions dutilisation", "getting_started.terms": "Conditions dutilisation",
"hashtag.column_header.tag_mode.all": "et {additional}", "hashtag.column_header.tag_mode.all": "et {additional}",
@ -204,7 +207,7 @@
"keyboard_shortcuts.column": "pour focaliser un statut dans lune des colonnes", "keyboard_shortcuts.column": "pour focaliser un statut dans lune des colonnes",
"keyboard_shortcuts.compose": "pour focaliser la zone de rédaction", "keyboard_shortcuts.compose": "pour focaliser la zone de rédaction",
"keyboard_shortcuts.description": "Description", "keyboard_shortcuts.description": "Description",
"keyboard_shortcuts.direct": "pour ouvrir la colonne des messages directs", "keyboard_shortcuts.direct": "pour ouvrir la colonne des messages privés",
"keyboard_shortcuts.down": "pour descendre dans la liste", "keyboard_shortcuts.down": "pour descendre dans la liste",
"keyboard_shortcuts.enter": "pour ouvrir le statut", "keyboard_shortcuts.enter": "pour ouvrir le statut",
"keyboard_shortcuts.favourite": "pour ajouter aux favoris", "keyboard_shortcuts.favourite": "pour ajouter aux favoris",
@ -219,6 +222,7 @@
"keyboard_shortcuts.muted": "pour ouvrir la liste des utilisateur·rice·s muté·e·s", "keyboard_shortcuts.muted": "pour ouvrir la liste des utilisateur·rice·s muté·e·s",
"keyboard_shortcuts.my_profile": "pour ouvrir votre profil", "keyboard_shortcuts.my_profile": "pour ouvrir votre profil",
"keyboard_shortcuts.notifications": "pour ouvrir votre colonne de notifications", "keyboard_shortcuts.notifications": "pour ouvrir votre colonne de notifications",
"keyboard_shortcuts.open_media": "pour ouvrir les médias",
"keyboard_shortcuts.pinned": "pour ouvrir une liste des pouets épinglés", "keyboard_shortcuts.pinned": "pour ouvrir une liste des pouets épinglés",
"keyboard_shortcuts.profile": "pour ouvrir le profil de lauteur·rice", "keyboard_shortcuts.profile": "pour ouvrir le profil de lauteur·rice",
"keyboard_shortcuts.reply": "pour répondre", "keyboard_shortcuts.reply": "pour répondre",
@ -251,9 +255,10 @@
"mute_modal.hide_notifications": "Masquer les notifications de cette personne?", "mute_modal.hide_notifications": "Masquer les notifications de cette personne?",
"navigation_bar.apps": "Applications mobiles", "navigation_bar.apps": "Applications mobiles",
"navigation_bar.blocks": "Comptes bloqués", "navigation_bar.blocks": "Comptes bloqués",
"navigation_bar.bookmarks": "Bookmarks",
"navigation_bar.community_timeline": "Fil public local", "navigation_bar.community_timeline": "Fil public local",
"navigation_bar.compose": "Rédiger un nouveau pouet", "navigation_bar.compose": "Rédiger un nouveau pouet",
"navigation_bar.direct": "Messages directs", "navigation_bar.direct": "Messages privés",
"navigation_bar.discover": "Découvrir", "navigation_bar.discover": "Découvrir",
"navigation_bar.domain_blocks": "Domaines cachés", "navigation_bar.domain_blocks": "Domaines cachés",
"navigation_bar.edit_profile": "Modifier le profil", "navigation_bar.edit_profile": "Modifier le profil",
@ -344,12 +349,13 @@
"status.admin_account": "Ouvrir linterface de modération pour @{name}", "status.admin_account": "Ouvrir linterface de modération pour @{name}",
"status.admin_status": "Ouvrir ce statut dans linterface de modération", "status.admin_status": "Ouvrir ce statut dans linterface de modération",
"status.block": "Bloquer @{name}", "status.block": "Bloquer @{name}",
"status.bookmark": "Marquer",
"status.cancel_reblog_private": "Dé-booster", "status.cancel_reblog_private": "Dé-booster",
"status.cannot_reblog": "Ce pouet ne peut pas être partagé", "status.cannot_reblog": "Ce pouet ne peut pas être partagé",
"status.copy": "Copier le lien vers le pouet", "status.copy": "Copier le lien vers le pouet",
"status.delete": "Effacer", "status.delete": "Effacer",
"status.detailed_status": "Vue détaillée de la conversation", "status.detailed_status": "Vue détaillée de la conversation",
"status.direct": "Envoyer un message direct à @{name}", "status.direct": "Envoyer un message privé à @{name}",
"status.embed": "Intégrer", "status.embed": "Intégrer",
"status.favourite": "Ajouter aux favoris", "status.favourite": "Ajouter aux favoris",
"status.filtered": "Filtré", "status.filtered": "Filtré",
@ -368,6 +374,7 @@
"status.reblogged_by": "{name} a partagé:", "status.reblogged_by": "{name} a partagé:",
"status.reblogs.empty": "Personne na encore partagé ce pouet. Lorsque quelquun le fera, il apparaîtra ici.", "status.reblogs.empty": "Personne na encore partagé ce pouet. Lorsque quelquun le fera, il apparaîtra ici.",
"status.redraft": "Effacer et ré-écrire", "status.redraft": "Effacer et ré-écrire",
"status.remove_bookmark": "Enlever le marque-page",
"status.reply": "Répondre", "status.reply": "Répondre",
"status.replyAll": "Répondre au fil", "status.replyAll": "Répondre au fil",
"status.report": "Signaler @{name}", "status.report": "Signaler @{name}",
@ -383,9 +390,9 @@
"status.unpin": "Retirer du profil", "status.unpin": "Retirer du profil",
"suggestions.dismiss": "Rejeter la suggestion", "suggestions.dismiss": "Rejeter la suggestion",
"suggestions.header": "Vous pourriez être intéressé·e par…", "suggestions.header": "Vous pourriez être intéressé·e par…",
"tabs_bar.federated_timeline": "Fil public global", "tabs_bar.federated_timeline": "Global",
"tabs_bar.home": "Accueil", "tabs_bar.home": "Accueil",
"tabs_bar.local_timeline": "Fil public local", "tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notifications", "tabs_bar.notifications": "Notifications",
"tabs_bar.search": "Chercher", "tabs_bar.search": "Chercher",
"time_remaining.days": "{number, plural, one {# day} other {# days}} restants", "time_remaining.days": "{number, plural, one {# day} other {# days}} restants",

View File

@ -1,21 +1,16 @@
import { import {
REBLOG_REQUEST,
REBLOG_FAIL,
FAVOURITE_REQUEST,
FAVOURITE_FAIL,
UNFAVOURITE_SUCCESS,
BOOKMARK_REQUEST,
BOOKMARK_FAIL, BOOKMARK_FAIL,
BOOKMARK_REQUEST,
FAVOURITE_FAIL,
FAVOURITE_REQUEST,
REBLOG_FAIL,
REBLOG_REQUEST,
UNFAVOURITE_SUCCESS,
} from '../actions/interactions'; } from '../actions/interactions';
import { import { STATUS_HIDE, STATUS_MUTE_SUCCESS, STATUS_REVEAL, STATUS_UNMUTE_SUCCESS } from '../actions/statuses';
STATUS_MUTE_SUCCESS,
STATUS_UNMUTE_SUCCESS,
STATUS_REVEAL,
STATUS_HIDE,
} from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines'; import { TIMELINE_DELETE } from '../actions/timelines';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer'; import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
import { Map as ImmutableMap, fromJS } from 'immutable'; import { fromJS, Map as ImmutableMap } from 'immutable';
const importStatus = (state, status) => state.set(status.id, fromJS(status)); const importStatus = (state, status) => state.set(status.id, fromJS(status));
@ -33,7 +28,7 @@ const deleteStatus = (state, id, references) => {
const initialState = ImmutableMap(); const initialState = ImmutableMap();
export default function statuses(state = initialState, action) { export default function statuses(state = initialState, action) {
switch(action.type) { switch (action.type) {
case STATUS_IMPORT: case STATUS_IMPORT:
return importStatus(state, action.status); return importStatus(state, action.status);
case STATUSES_IMPORT: case STATUSES_IMPORT:

View File

@ -0,0 +1,29 @@
/** @license
DHTML Snowstorm! JavaScript-based snow for web pages
Making it snow on the internets since 2003. You're welcome.
-----------------------------------------------------------
Version 1.44.20131208 (Previous rev: 1.44.20131125)
Copyright (c) 2007, Scott Schiller. All rights reserved.
Code provided under the BSD License
http://schillmania.com/projects/snowstorm/license.txt
*/
var snowStorm=function(g,f){function k(a,d){isNaN(d)&&(d=0);return Math.random()*a+d}function x(){g.setTimeout(function(){a.start(!0)},20);a.events.remove(m?f:g,"mousemove",x)}function y(){(!a.excludeMobile||!D)&&x();a.events.remove(g,"load",y)}this.excludeMobile=this.autoStart=!0;this.flakesMax=128;this.flakesMaxActive=64;this.animationInterval=33;this.useGPU=!0;this.className=null;this.excludeMobile=!0;this.flakeBottom=null;this.followMouse=!0;this.snowColor="#fff";this.snowCharacter="&bull;";this.snowStick=
!0;this.targetElement=null;this.useMeltEffect=!0;this.usePixelPosition=this.usePositionFixed=this.useTwinkleEffect=!1;this.freezeOnBlur=!0;this.flakeRightOffset=this.flakeLeftOffset=0;this.flakeHeight=this.flakeWidth=8;this.vMaxX=5;this.vMaxY=4;this.zIndex=0;var a=this,q,m=navigator.userAgent.match(/msie/i),E=navigator.userAgent.match(/msie 6/i),D=navigator.userAgent.match(/mobile|opera m(ob|in)/i),r=m&&"BackCompat"===f.compatMode||E,h=null,n=null,l=null,p=null,s=null,z=null,A=null,v=1,t=!1,w=!1,
u;a:{try{f.createElement("div").style.opacity="0.5"}catch(F){u=!1;break a}u=!0}var B=!1,C=f.createDocumentFragment();q=function(){function c(b){g.setTimeout(b,1E3/(a.animationInterval||20))}function d(a){return void 0!==h.style[a]?a:null}var e,b=g.requestAnimationFrame||g.webkitRequestAnimationFrame||g.mozRequestAnimationFrame||g.oRequestAnimationFrame||g.msRequestAnimationFrame||c;e=b?function(){return b.apply(g,arguments)}:null;var h;h=f.createElement("div");e={transform:{ie:d("-ms-transform"),
moz:d("MozTransform"),opera:d("OTransform"),webkit:d("webkitTransform"),w3:d("transform"),prop:null},getAnimationFrame:e};e.transform.prop=e.transform.w3||e.transform.moz||e.transform.webkit||e.transform.ie||e.transform.opera;h=null;return e}();this.timer=null;this.flakes=[];this.active=this.disabled=!1;this.meltFrameCount=20;this.meltFrames=[];this.setXY=function(c,d,e){if(!c)return!1;a.usePixelPosition||w?(c.style.left=d-a.flakeWidth+"px",c.style.top=e-a.flakeHeight+"px"):r?(c.style.right=100-100*
(d/h)+"%",c.style.top=Math.min(e,s-a.flakeHeight)+"px"):a.flakeBottom?(c.style.right=100-100*(d/h)+"%",c.style.top=Math.min(e,s-a.flakeHeight)+"px"):(c.style.right=100-100*(d/h)+"%",c.style.bottom=100-100*(e/l)+"%")};this.events=function(){function a(c){c=b.call(c);var d=c.length;e?(c[1]="on"+c[1],3<d&&c.pop()):3===d&&c.push(!1);return c}function d(a,b){var c=a.shift(),d=[f[b]];if(e)c[d](a[0],a[1]);else c[d].apply(c,a)}var e=!g.addEventListener&&g.attachEvent,b=Array.prototype.slice,f={add:e?"attachEvent":
"addEventListener",remove:e?"detachEvent":"removeEventListener"};return{add:function(){d(a(arguments),"add")},remove:function(){d(a(arguments),"remove")}}}();this.randomizeWind=function(){var c;c=k(a.vMaxX,0.2);z=1===parseInt(k(2),10)?-1*c:c;A=k(a.vMaxY,0.2);if(this.flakes)for(c=0;c<this.flakes.length;c++)this.flakes[c].active&&this.flakes[c].setVelocities()};this.scrollHandler=function(){var c;p=a.flakeBottom?0:parseInt(g.scrollY||f.documentElement.scrollTop||(r?f.body.scrollTop:0),10);isNaN(p)&&
(p=0);if(!t&&!a.flakeBottom&&a.flakes)for(c=0;c<a.flakes.length;c++)0===a.flakes[c].active&&a.flakes[c].stick()};this.resizeHandler=function(){g.innerWidth||g.innerHeight?(h=g.innerWidth-16-a.flakeRightOffset,l=a.flakeBottom||g.innerHeight):(h=(f.documentElement.clientWidth||f.body.clientWidth||f.body.scrollWidth)-(!m?8:0)-a.flakeRightOffset,l=a.flakeBottom||f.documentElement.clientHeight||f.body.clientHeight||f.body.scrollHeight);s=f.body.offsetHeight;n=parseInt(h/2,10)};this.resizeHandlerAlt=function(){h=
a.targetElement.offsetWidth-a.flakeRightOffset;l=a.flakeBottom||a.targetElement.offsetHeight;n=parseInt(h/2,10);s=f.body.offsetHeight};this.freeze=function(){if(a.disabled)return!1;a.disabled=1;a.timer=null};this.resume=function(){if(a.disabled)a.disabled=0;else return!1;a.timerInit()};this.toggleSnow=function(){a.flakes.length?(a.active=!a.active,a.active?(a.show(),a.resume()):(a.stop(),a.freeze())):a.start()};this.stop=function(){var c;this.freeze();for(c=0;c<this.flakes.length;c++)this.flakes[c].o.style.display=
"none";a.events.remove(g,"scroll",a.scrollHandler);a.events.remove(g,"resize",a.resizeHandler);a.freezeOnBlur&&(m?(a.events.remove(f,"focusout",a.freeze),a.events.remove(f,"focusin",a.resume)):(a.events.remove(g,"blur",a.freeze),a.events.remove(g,"focus",a.resume)))};this.show=function(){var a;for(a=0;a<this.flakes.length;a++)this.flakes[a].o.style.display="block"};this.SnowFlake=function(c,d,e){var b=this;this.type=c;this.x=d||parseInt(k(h-20),10);this.y=!isNaN(e)?e:-k(l)-12;this.vY=this.vX=null;
this.vAmpTypes=[1,1.2,1.4,1.6,1.8];this.vAmp=this.vAmpTypes[this.type]||1;this.melting=!1;this.meltFrameCount=a.meltFrameCount;this.meltFrames=a.meltFrames;this.twinkleFrame=this.meltFrame=0;this.active=1;this.fontSize=10+10*(this.type/5);this.o=f.createElement("div");this.o.innerHTML=a.snowCharacter;a.className&&this.o.setAttribute("class",a.className);this.o.style.color=a.snowColor;this.o.style.position=t?"fixed":"absolute";a.useGPU&&q.transform.prop&&(this.o.style[q.transform.prop]="translate3d(0px, 0px, 0px)");
this.o.style.width=a.flakeWidth+"px";this.o.style.height=a.flakeHeight+"px";this.o.style.fontFamily="arial,verdana";this.o.style.cursor="default";this.o.style.overflow="hidden";this.o.style.fontWeight="normal";this.o.style.zIndex=a.zIndex;C.appendChild(this.o);this.refresh=function(){if(isNaN(b.x)||isNaN(b.y))return!1;a.setXY(b.o,b.x,b.y)};this.stick=function(){r||a.targetElement!==f.documentElement&&a.targetElement!==f.body?b.o.style.top=l+p-a.flakeHeight+"px":a.flakeBottom?b.o.style.top=a.flakeBottom+
"px":(b.o.style.display="none",b.o.style.bottom="0%",b.o.style.position="fixed",b.o.style.display="block")};this.vCheck=function(){0<=b.vX&&0.2>b.vX?b.vX=0.2:0>b.vX&&-0.2<b.vX&&(b.vX=-0.2);0<=b.vY&&0.2>b.vY&&(b.vY=0.2)};this.move=function(){var c=b.vX*v;b.x+=c;b.y+=b.vY*b.vAmp;b.x>=h||h-b.x<a.flakeWidth?b.x=0:0>c&&b.x-a.flakeLeftOffset<-a.flakeWidth&&(b.x=h-a.flakeWidth-1);b.refresh();l+p-b.y+a.flakeHeight<a.flakeHeight?(b.active=0,a.snowStick?b.stick():b.recycle()):(a.useMeltEffect&&(b.active&&3>
b.type&&!b.melting&&0.998<Math.random())&&(b.melting=!0,b.melt()),a.useTwinkleEffect&&(0>b.twinkleFrame?0.97<Math.random()&&(b.twinkleFrame=parseInt(8*Math.random(),10)):(b.twinkleFrame--,u?b.o.style.opacity=b.twinkleFrame&&0===b.twinkleFrame%2?0:1:b.o.style.visibility=b.twinkleFrame&&0===b.twinkleFrame%2?"hidden":"visible")))};this.animate=function(){b.move()};this.setVelocities=function(){b.vX=z+k(0.12*a.vMaxX,0.1);b.vY=A+k(0.12*a.vMaxY,0.1)};this.setOpacity=function(a,b){if(!u)return!1;a.style.opacity=
b};this.melt=function(){!a.useMeltEffect||!b.melting?b.recycle():b.meltFrame<b.meltFrameCount?(b.setOpacity(b.o,b.meltFrames[b.meltFrame]),b.o.style.fontSize=b.fontSize-b.fontSize*(b.meltFrame/b.meltFrameCount)+"px",b.o.style.lineHeight=a.flakeHeight+2+0.75*a.flakeHeight*(b.meltFrame/b.meltFrameCount)+"px",b.meltFrame++):b.recycle()};this.recycle=function(){b.o.style.display="none";b.o.style.position=t?"fixed":"absolute";b.o.style.bottom="auto";b.setVelocities();b.vCheck();b.meltFrame=0;b.melting=
!1;b.setOpacity(b.o,1);b.o.style.padding="0px";b.o.style.margin="0px";b.o.style.fontSize=b.fontSize+"px";b.o.style.lineHeight=a.flakeHeight+2+"px";b.o.style.textAlign="center";b.o.style.verticalAlign="baseline";b.x=parseInt(k(h-a.flakeWidth-20),10);b.y=parseInt(-1*k(l),10)-a.flakeHeight;b.refresh();b.o.style.display="block";b.active=1};this.recycle();this.refresh()};this.snow=function(){var c=0,d=null,e,d=0;for(e=a.flakes.length;d<e;d++)1===a.flakes[d].active&&(a.flakes[d].move(),c++),a.flakes[d].melting&&
a.flakes[d].melt();c<a.flakesMaxActive&&(d=a.flakes[parseInt(k(a.flakes.length),10)],0===d.active&&(d.melting=!0));a.timer&&q.getAnimationFrame(a.snow)};this.mouseMove=function(c){if(!a.followMouse)return!0;c=parseInt(c.clientX,10);c<n?v=-2+2*(c/n):(c-=n,v=2*(c/n))};this.createSnow=function(c,d){var e;for(e=0;e<c;e++)if(a.flakes[a.flakes.length]=new a.SnowFlake(parseInt(k(6),10)),d||e>a.flakesMaxActive)a.flakes[a.flakes.length-1].active=-1;a.targetElement.appendChild(C)};this.timerInit=function(){a.timer=
!0;a.snow()};this.init=function(){var c;for(c=0;c<a.meltFrameCount;c++)a.meltFrames.push(1-c/a.meltFrameCount);a.randomizeWind();a.createSnow(a.flakesMax);a.events.add(g,"resize",a.resizeHandler);a.events.add(g,"scroll",a.scrollHandler);a.freezeOnBlur&&(m?(a.events.add(f,"focusout",a.freeze),a.events.add(f,"focusin",a.resume)):(a.events.add(g,"blur",a.freeze),a.events.add(g,"focus",a.resume)));a.resizeHandler();a.scrollHandler();a.followMouse&&a.events.add(m?f:g,"mousemove",a.mouseMove);a.animationInterval=
Math.max(20,a.animationInterval);a.timerInit()};this.start=function(c){if(B){if(c)return!0}else B=!0;if("string"===typeof a.targetElement&&(c=a.targetElement,a.targetElement=f.getElementById(c),!a.targetElement))throw Error('Snowstorm: Unable to get targetElement "'+c+'"');a.targetElement||(a.targetElement=f.body||f.documentElement);a.targetElement!==f.documentElement&&a.targetElement!==f.body&&(a.resizeHandler=a.resizeHandlerAlt,a.usePixelPosition=!0);a.resizeHandler();a.usePositionFixed=a.usePositionFixed&&
!r&&!a.flakeBottom;if(g.getComputedStyle)try{w="relative"===g.getComputedStyle(a.targetElement,null).getPropertyValue("position")}catch(d){w=!1}t=a.usePositionFixed;h&&(l&&!a.disabled)&&(a.init(),a.active=!0)};a.autoStart&&a.events.add(g,"load",y,!1);return this}(window, document);

View File

@ -0,0 +1,30 @@
@import 'bliss/variables';
@import 'bliss/mixins';
@import 'bliss/variables';
@import 'fonts/roboto';
@import 'fonts/roboto-mono';
@import 'fonts/montserrat';
@import 'bliss/reset';
@import 'bliss/messaging';
@import 'bliss/basics';
@import 'bliss/containers';
@import 'bliss/lists';
@import 'bliss/footer';
@import 'bliss/compact_header';
@import 'bliss/widgets';
@import 'bliss/forms';
@import 'bliss/accounts';
@import 'bliss/statuses';
@import 'bliss/boost';
@import 'bliss/components';
@import 'bliss/polls';
@import 'bliss/introduction';
@import 'bliss/modal';
@import 'bliss/emoji_picker';
@import 'bliss/about';
@import 'bliss/tables';
@import 'bliss/admin';
@import 'bliss/dashboard';
@import 'bliss/rtl';
@import 'bliss/accessibility';

View File

@ -0,0 +1,74 @@
@mixin avatar-radius {
border-radius: 4px;
background: transparent no-repeat;
background-position: 50%;
background-clip: padding-box;
}
@mixin avatar-size($size: 48px) {
width: $size;
height: $size;
background-size: $size $size;
}
@mixin search-input {
outline: 0;
box-sizing: border-box;
width: 100%;
border: 0;
box-shadow: none;
font-family: inherit;
background: $ui-base-color;
color: $darker-text-color;
font-size: 14px;
margin: 0;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
@mixin search-popout {
background: $simple-background-color;
border-radius: 4px;
padding: 10px 14px;
padding-bottom: 14px;
margin-top: 10px;
color: $light-text-color;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
h4 {
text-transform: uppercase;
color: $light-text-color;
font-size: 13px;
font-weight: 500;
margin-bottom: 10px;
}
li {
padding: 4px 0;
}
ul {
margin-bottom: 10px;
}
em {
font-weight: 500;
color: $inverted-text-color;
}
}

View File

@ -0,0 +1,856 @@
$maximum-width: 1235px;
$fluid-breakpoint: $maximum-width + 20px;
$column-breakpoint: 700px;
$small-breakpoint: 960px;
.container {
box-sizing: border-box;
max-width: $maximum-width;
margin: 0 auto;
position: relative;
@media screen and (max-width: $fluid-breakpoint) {
width: 100%;
padding: 0 10px;
}
}
.rich-formatting {
font-family: $font-sans-serif, sans-serif;
font-size: 16px;
font-weight: 400;
font-size: 16px;
line-height: 30px;
color: $darker-text-color;
padding-right: 10px;
a {
color: $highlight-text-color;
text-decoration: underline;
}
p,
li {
font-family: $font-sans-serif, sans-serif;
font-size: 16px;
font-weight: 400;
font-size: 16px;
line-height: 30px;
margin-bottom: 12px;
color: $darker-text-color;
a {
color: $highlight-text-color;
text-decoration: underline;
}
&:last-child {
margin-bottom: 0;
}
}
strong,
em {
font-weight: 700;
color: lighten($darker-text-color, 10%);
}
h1 {
font-family: $font-display, sans-serif;
font-size: 26px;
line-height: 30px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
small {
font-family: $font-sans-serif, sans-serif;
display: block;
font-size: 18px;
font-weight: 400;
color: lighten($darker-text-color, 10%);
}
}
h2 {
font-family: $font-display, sans-serif;
font-size: 22px;
line-height: 26px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
h3 {
font-family: $font-display, sans-serif;
font-size: 18px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
h4 {
font-family: $font-display, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
h5 {
font-family: $font-display, sans-serif;
font-size: 14px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
h6 {
font-family: $font-display, sans-serif;
font-size: 12px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
ul,
ol {
margin-left: 20px;
&[type='a'] {
list-style-type: lower-alpha;
}
&[type='i'] {
list-style-type: lower-roman;
}
}
ul {
list-style: disc;
}
ol {
list-style: decimal;
}
li > ol,
li > ul {
margin-top: 6px;
}
hr {
width: 100%;
height: 0;
border: 0;
border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
margin: 20px 0;
&.spacer {
height: 1px;
border: 0;
}
}
}
.information-board {
background: darken($ui-base-color, 4%);
padding: 20px 0;
.container-alt {
position: relative;
padding-right: 280px + 15px;
}
&__sections {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
&__section {
flex: 1 0 0;
font-family: $font-sans-serif, sans-serif;
font-size: 16px;
line-height: 28px;
color: $primary-text-color;
text-align: right;
padding: 10px 15px;
span,
strong {
display: block;
}
span {
&:last-child {
color: $secondary-text-color;
}
}
strong {
font-family: $font-display, sans-serif;
font-weight: 500;
font-size: 32px;
line-height: 48px;
}
@media screen and (max-width: $column-breakpoint) {
text-align: center;
}
}
.panel {
position: absolute;
width: 280px;
box-sizing: border-box;
background: darken($ui-base-color, 8%);
padding: 20px;
padding-top: 10px;
border-radius: 4px 4px 0 0;
right: 0;
bottom: -40px;
.panel-header {
font-family: $font-display, sans-serif;
font-size: 14px;
line-height: 24px;
font-weight: 500;
color: $darker-text-color;
padding-bottom: 5px;
margin-bottom: 15px;
border-bottom: 1px solid lighten($ui-base-color, 4%);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
a,
span {
font-weight: 400;
color: darken($darker-text-color, 10%);
}
a {
text-decoration: none;
}
}
}
.owner {
text-align: center;
.avatar {
width: 80px;
height: 80px;
margin: 0 auto;
margin-bottom: 15px;
img {
display: block;
width: 80px;
height: 80px;
border-radius: 48px;
}
}
.name {
font-size: 14px;
a {
display: block;
color: $primary-text-color;
text-decoration: none;
&:hover {
.display_name {
text-decoration: underline;
}
}
}
.username {
display: block;
color: $darker-text-color;
}
}
}
}
.landing-page {
p,
li {
font-family: $font-sans-serif, sans-serif;
font-size: 16px;
font-weight: 400;
font-size: 16px;
line-height: 30px;
margin-bottom: 12px;
color: $darker-text-color;
a {
color: $highlight-text-color;
text-decoration: underline;
}
}
em {
display: inline;
margin: 0;
padding: 0;
font-weight: 700;
background: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: lighten($darker-text-color, 10%);
}
h1 {
font-family: $font-display, sans-serif;
font-size: 26px;
line-height: 30px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
small {
font-family: $font-sans-serif, sans-serif;
display: block;
font-size: 18px;
font-weight: 400;
color: lighten($darker-text-color, 10%);
}
}
h2 {
font-family: $font-display, sans-serif;
font-size: 22px;
line-height: 26px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
h3 {
font-family: $font-display, sans-serif;
font-size: 18px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
h4 {
font-family: $font-display, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
h5 {
font-family: $font-display, sans-serif;
font-size: 14px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
h6 {
font-family: $font-display, sans-serif;
font-size: 12px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
ul,
ol {
margin-left: 20px;
&[type='a'] {
list-style-type: lower-alpha;
}
&[type='i'] {
list-style-type: lower-roman;
}
}
ul {
list-style: disc;
}
ol {
list-style: decimal;
}
li > ol,
li > ul {
margin-top: 6px;
}
hr {
width: 100%;
height: 0;
border: 0;
border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
margin: 20px 0;
&.spacer {
height: 1px;
border: 0;
}
}
&__information,
&__forms {
padding: 20px;
}
&__call-to-action {
background: darken($ui-base-color, 4%);
border-radius: 4px;
padding: 25px 40px;
overflow: hidden;
box-sizing: border-box;
.row {
width: 100%;
display: flex;
flex-direction: row-reverse;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
}
.row__information-board {
display: flex;
justify-content: flex-end;
align-items: flex-end;
.information-board__section {
flex: 1 0 auto;
padding: 0 10px;
}
@media screen and (max-width: $no-gap-breakpoint) {
width: 100%;
justify-content: space-between;
}
}
.row__mascot {
flex: 1;
margin: 10px -50px 0 0;
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
}
&__logo {
margin-right: 20px;
img {
height: 50px;
width: auto;
mix-blend-mode: lighten;
}
}
&__information {
padding: 45px 40px;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
strong {
font-weight: 500;
color: lighten($darker-text-color, 10%);
}
.account {
border-bottom: 0;
padding: 0;
&__display-name {
align-items: center;
display: flex;
margin-right: 5px;
}
div.account__display-name {
&:hover {
.display-name strong {
text-decoration: none;
}
}
.account__avatar {
cursor: default;
}
}
&__avatar-wrapper {
margin-left: 0;
flex: 0 0 auto;
}
&__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
}
.display-name {
font-size: 15px;
&__account {
font-size: 14px;
}
}
}
@media screen and (max-width: $small-breakpoint) {
.contact {
margin-top: 30px;
}
}
@media screen and (max-width: $column-breakpoint) {
padding: 25px 20px;
}
}
&__information,
&__forms,
#mastodon-timeline {
box-sizing: border-box;
background: $ui-base-color;
border-radius: 4px;
box-shadow: 0 0 6px rgba($black, 0.1);
}
&__mascot {
height: 104px;
position: relative;
left: -40px;
bottom: 25px;
img {
height: 190px;
width: auto;
}
}
&__short-description {
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 40px;
}
@media screen and (max-width: $column-breakpoint) {
.row {
margin-bottom: 20px;
}
}
p a {
color: $secondary-text-color;
}
h1 {
font-weight: 500;
color: $primary-text-color;
margin-bottom: 0;
small {
color: $darker-text-color;
span {
color: $secondary-text-color;
}
}
}
p:last-child {
margin-bottom: 0;
}
}
&__hero {
margin-bottom: 10px;
img {
display: block;
margin: 0;
max-width: 100%;
height: auto;
border-radius: 4px;
}
}
@media screen and (max-width: 840px) {
.information-board {
.container-alt {
padding-right: 20px;
}
.panel {
position: static;
margin-top: 20px;
width: 100%;
border-radius: 4px;
.panel-header {
text-align: center;
}
}
}
}
@media screen and (max-width: 675px) {
.header-wrapper {
padding-top: 0;
&.compact {
padding-bottom: 0;
}
&.compact .hero .heading {
text-align: initial;
}
}
.header .container-alt,
.features .container-alt {
display: block;
}
}
.cta {
margin: 20px;
}
}
.landing {
margin-bottom: 100px;
@media screen and (max-width: 738px) {
margin-bottom: 0;
}
&__brand {
display: flex;
justify-content: center;
align-items: center;
padding: 50px;
svg {
fill: $primary-text-color;
height: 52px;
}
@media screen and (max-width: $no-gap-breakpoint) {
padding: 0;
margin-bottom: 30px;
}
}
.directory {
margin-top: 30px;
background: transparent;
box-shadow: none;
border-radius: 0;
}
.hero-widget {
margin-top: 30px;
margin-bottom: 0;
h4 {
padding: 10px;
text-transform: uppercase;
font-weight: 700;
font-size: 13px;
color: $darker-text-color;
}
&__text {
border-radius: 0;
padding-bottom: 0;
}
&__footer {
background: $ui-base-color;
padding: 10px;
border-radius: 0 0 4px 4px;
display: flex;
&__column {
flex: 1 1 50%;
}
}
.account {
padding: 10px 0;
border-bottom: 0;
.account__display-name {
display: flex;
align-items: center;
}
.account__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
}
}
&__counter {
padding: 10px;
strong {
font-family: $font-display, sans-serif;
font-size: 15px;
font-weight: 700;
display: block;
}
span {
font-size: 14px;
color: $darker-text-color;
}
}
}
.simple_form .user_agreement .label_input > label {
font-weight: 400;
color: $darker-text-color;
}
.simple_form p.lead {
color: $darker-text-color;
font-size: 15px;
line-height: 20px;
font-weight: 400;
margin-bottom: 25px;
}
&__grid {
max-width: 960px;
margin: 0 auto;
display: grid;
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
grid-gap: 30px;
@media screen and (max-width: 738px) {
grid-template-columns: minmax(0, 100%);
grid-gap: 10px;
&__column-login {
grid-row: 1;
display: flex;
flex-direction: column;
.box-widget {
order: 2;
flex: 0 0 auto;
}
.hero-widget {
margin-top: 0;
margin-bottom: 10px;
order: 1;
flex: 0 0 auto;
}
}
&__column-registration {
grid-row: 2;
}
.directory {
margin-top: 10px;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
grid-gap: 0;
.hero-widget {
display: block;
margin-bottom: 0;
box-shadow: none;
&__img,
&__img img,
&__footer {
border-radius: 0;
}
}
.hero-widget,
.box-widget,
.directory__tag {
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.directory {
margin-top: 0;
&__tag {
margin-bottom: 0;
& > a,
& > div {
border-radius: 0;
box-shadow: none;
}
&:last-child {
border-bottom: 0;
}
}
}
}
}
}
.brand {
position: relative;
text-decoration: none;
}
.brand__tagline {
display: block;
position: absolute;
bottom: -10px;
left: 50px;
width: 300px;
color: $ui-primary-color;
text-decoration: none;
font-size: 14px;
@media screen and (max-width: $no-gap-breakpoint) {
position: static;
width: auto;
margin-top: 20px;
color: $dark-text-color;
}
}

View File

@ -0,0 +1,18 @@
$black-emojis: '8ball' 'ant' 'back' 'black_circle' 'black_heart' 'black_large_square' 'black_medium_small_square' 'black_medium_square' 'black_nib' 'black_small_square' 'bomb' 'bowling' 'bust_in_silhouette' 'busts_in_silhouette' 'camera' 'camera_with_flash' 'clubs' 'copyright' 'curly_loop' 'currency_exchange' 'dark_sunglasses' 'eight_pointed_black_star' 'electric_plug' 'end' 'female-guard' 'film_projector' 'fried_egg' 'gorilla' 'guardsman' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'hocho' 'hole' 'joystick' 'kaaba' 'lower_left_ballpoint_pen' 'lower_left_fountain_pen' 'male-guard' 'microphone' 'mortar_board' 'movie_camera' 'musical_score' 'on' 'registered' 'soon' 'spades' 'speaking_head_in_silhouette' 'spider' 'telephone_receiver' 'tm' 'top' 'tophat' 'turkey' 'vhs' 'video_camera' 'video_game' 'water_buffalo' 'waving_black_flag' 'wavy_dash';
%white-emoji-outline {
filter: drop-shadow(1px 1px 0 $white) drop-shadow(-1px 1px 0 $white) drop-shadow(1px -1px 0 $white) drop-shadow(-1px -1px 0 $white);
transform: scale(.71);
}
.emojione {
@each $emoji in $black-emojis {
&[title=':#{$emoji}:'] {
@extend %white-emoji-outline;
}
}
}
.fa {
margin-right: 1ch;
}

View File

@ -0,0 +1,322 @@
.card {
& > a {
display: block;
text-decoration: none;
color: inherit;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
@media screen and (max-width: $no-gap-breakpoint) {
box-shadow: none;
}
&:hover,
&:active,
&:focus {
.card__bar {
background: lighten($ui-base-color, 8%);
}
}
}
&__img {
height: 130px;
position: relative;
background: darken($ui-base-color, 12%);
border-radius: 4px 4px 0 0;
img {
display: block;
width: 100%;
height: 100%;
margin: 0;
object-fit: cover;
border-radius: 4px 4px 0 0;
}
@media screen and (max-width: 600px) {
height: 200px;
}
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
&__bar {
position: relative;
padding: 15px;
display: flex;
justify-content: flex-start;
align-items: center;
background: lighten($ui-base-color, 4%);
border-radius: 0 0 4px 4px;
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 0;
}
.avatar {
flex: 0 0 auto;
width: 48px;
height: 48px;
padding-top: 2px;
img {
width: 100%;
height: 100%;
display: block;
margin: 0;
border-radius: 4px;
background: darken($ui-base-color, 8%);
object-fit: cover;
}
}
.display-name {
margin-left: 15px;
text-align: left;
strong {
font-size: 15px;
color: $primary-text-color;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
}
span {
display: block;
font-size: 14px;
color: $darker-text-color;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
.pagination {
padding: 30px 0;
text-align: center;
overflow: hidden;
a,
.current,
.newer,
.older,
.page,
.gap {
font-size: 14px;
color: $primary-text-color;
font-weight: 500;
display: inline-block;
padding: 6px 10px;
text-decoration: none;
}
.current {
background: $simple-background-color;
border-radius: 100px;
color: $inverted-text-color;
cursor: default;
margin: 0 10px;
}
.gap {
cursor: default;
}
.older,
.newer {
text-transform: uppercase;
color: $secondary-text-color;
}
.older {
float: left;
padding-left: 0;
.fa {
display: inline-block;
margin-right: 5px;
}
}
.newer {
float: right;
padding-right: 0;
.fa {
display: inline-block;
margin-left: 5px;
}
}
.disabled {
cursor: default;
color: lighten($inverted-text-color, 10%);
}
@media screen and (max-width: 700px) {
padding: 30px 20px;
.page {
display: none;
}
.newer,
.older {
display: inline-block;
}
}
}
.nothing-here {
background: $ui-base-color;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
color: $light-text-color;
font-size: 14px;
font-weight: 500;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
cursor: default;
border-radius: 4px;
padding: 20px;
min-height: 30vh;
&--under-tabs {
border-radius: 0 0 4px 4px;
}
&--flexible {
box-sizing: border-box;
min-height: 100%;
}
}
.account-role,
.simple_form .recommended {
display: inline-block;
padding: 4px 6px;
cursor: default;
border-radius: 3px;
font-size: 12px;
line-height: 12px;
font-weight: 500;
color: $ui-secondary-color;
background-color: rgba($ui-secondary-color, 0.1);
border: 1px solid rgba($ui-secondary-color, 0.5);
&.moderator {
color: $success-green;
background-color: rgba($success-green, 0.1);
border-color: rgba($success-green, 0.5);
}
&.admin {
color: lighten($error-red, 12%);
background-color: rgba(lighten($error-red, 12%), 0.1);
border-color: rgba(lighten($error-red, 12%), 0.5);
}
}
.account__header__fields {
padding: 0;
margin: 15px -15px -15px;
border: 0 none;
border-top: 1px solid lighten($ui-base-color, 12%);
border-bottom: 1px solid lighten($ui-base-color, 12%);
font-size: 14px;
line-height: 20px;
dl {
display: flex;
border-bottom: 1px solid lighten($ui-base-color, 12%);
}
dt,
dd {
box-sizing: border-box;
padding: 14px;
text-align: center;
max-height: 48px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
dt {
font-weight: 500;
width: 120px;
flex: 0 0 auto;
color: $secondary-text-color;
background: rgba(darken($ui-base-color, 8%), 0.5);
}
dd {
flex: 1 1 auto;
color: $darker-text-color;
}
a {
color: $highlight-text-color;
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
.verified {
border: 1px solid rgba($valid-value-color, 0.5);
background: rgba($valid-value-color, 0.25);
a {
color: $valid-value-color;
font-weight: 500;
}
&__mark {
color: $valid-value-color;
}
}
dl:last-child {
border-bottom: 0;
}
}
.directory__tag .trends__item__current {
width: auto;
}
.pending-account {
&__header {
color: $darker-text-color;
a {
color: $ui-secondary-color;
text-decoration: none;
&:hover,
&:active,
&:focus {
text-decoration: underline;
}
}
strong {
color: $primary-text-color;
font-weight: 700;
}
}
&__body {
margin-top: 10px;
}
}

View File

@ -0,0 +1,718 @@
$no-columns-breakpoint: 600px;
$sidebar-width: 240px;
$content-width: 840px;
.admin-wrapper {
display: flex;
justify-content: center;
height: 100%;
.sidebar-wrapper {
flex: 1 1 $sidebar-width;
height: 100%;
background: $ui-base-color;
display: flex;
justify-content: flex-end;
}
.sidebar {
width: $sidebar-width;
height: 100%;
padding: 0;
overflow-y: auto;
.logo {
display: block;
margin: 40px auto;
width: 100px;
height: 100px;
}
@media screen and (max-width: $no-columns-breakpoint) {
& > a:first-child {
display: none;
}
}
ul {
list-style: none;
border-radius: 4px 0 0 4px;
overflow: hidden;
margin-bottom: 20px;
@media screen and (max-width: $no-columns-breakpoint) {
margin-bottom: 0;
}
a {
display: block;
padding: 15px;
color: $darker-text-color;
text-decoration: none;
transition: all 200ms linear;
transition-property: color, background-color;
border-radius: 4px 0 0 4px;
i.fa {
margin-right: 5px;
}
&:hover {
color: $primary-text-color;
background-color: darken($ui-base-color, 5%);
transition: all 100ms linear;
transition-property: color, background-color;
}
&.selected {
background: darken($ui-base-color, 2%);
border-radius: 4px 0 0;
}
}
ul {
background: darken($ui-base-color, 4%);
border-radius: 0 0 0 4px;
margin: 0;
a {
border: 0;
padding: 15px 35px;
}
}
.simple-navigation-active-leaf a {
color: $primary-text-color;
background-color: $ui-highlight-color;
border-bottom: 0;
border-radius: 0;
&:hover {
background-color: lighten($ui-highlight-color, 5%);
}
}
}
& > ul > .simple-navigation-active-leaf a {
border-radius: 4px 0 0 4px;
}
}
.content-wrapper {
flex: 2 1 $content-width;
overflow: auto;
}
.content {
max-width: $content-width;
padding: 20px 15px;
padding-top: 60px;
padding-left: 25px;
@media screen and (max-width: $no-columns-breakpoint) {
max-width: none;
padding: 15px;
padding-top: 30px;
}
h2 {
color: $secondary-text-color;
font-size: 24px;
line-height: 28px;
font-weight: 400;
padding-bottom: 40px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
margin-bottom: 40px;
}
h3 {
color: $secondary-text-color;
font-size: 20px;
line-height: 28px;
font-weight: 400;
margin-bottom: 30px;
}
h4 {
text-transform: uppercase;
font-size: 13px;
font-weight: 700;
color: $darker-text-color;
padding-bottom: 8px;
margin-bottom: 8px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
h6 {
font-size: 16px;
color: $secondary-text-color;
line-height: 28px;
font-weight: 400;
}
.fields-group h6 {
color: $primary-text-color;
font-weight: 500;
}
.directory__tag > a,
.directory__tag > div {
box-shadow: none;
}
.directory__tag .table-action-link .fa {
color: inherit;
}
.directory__tag h4 {
font-size: 18px;
font-weight: 700;
color: $primary-text-color;
text-transform: none;
padding-bottom: 0;
margin-bottom: 0;
border-bottom: 0;
}
& > p {
font-size: 14px;
line-height: 18px;
color: $secondary-text-color;
margin-bottom: 20px;
strong {
color: $primary-text-color;
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
}
hr {
width: 100%;
height: 0;
border: 0;
border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
margin: 20px 0;
&.spacer {
height: 1px;
border: 0;
}
}
}
@media screen and (max-width: $no-columns-breakpoint) {
display: block;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
.sidebar-wrapper,
.content-wrapper {
flex: 0 0 auto;
height: auto;
overflow: initial;
}
.sidebar {
width: 100%;
padding: 0;
height: auto;
}
}
}
hr.spacer {
width: 100%;
border: 0;
margin: 20px 0;
height: 1px;
}
.muted-hint {
color: $darker-text-color;
a {
color: $highlight-text-color;
}
}
.positive-hint {
color: $valid-value-color;
font-weight: 500;
}
.negative-hint {
color: $error-value-color;
font-weight: 500;
}
.neutral-hint {
color: $dark-text-color;
font-weight: 500;
}
.warning-hint {
color: $gold-star;
font-weight: 500;
}
.filters {
display: flex;
flex-wrap: wrap;
.filter-subset {
flex: 0 0 auto;
margin: 0 40px 10px 0;
&:last-child {
margin-bottom: 20px;
}
ul {
margin-top: 5px;
list-style: none;
li {
display: inline-block;
margin-right: 5px;
}
}
strong {
font-weight: 500;
text-transform: uppercase;
font-size: 12px;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
a {
display: inline-block;
color: $darker-text-color;
text-decoration: none;
text-transform: uppercase;
font-size: 12px;
font-weight: 500;
border-bottom: 2px solid $ui-base-color;
&:hover {
color: $primary-text-color;
border-bottom: 2px solid lighten($ui-base-color, 5%);
}
&.selected {
color: $highlight-text-color;
border-bottom: 2px solid $ui-highlight-color;
}
}
}
}
.report-accounts {
display: flex;
flex-wrap: wrap;
margin-bottom: 20px;
}
.report-accounts__item {
display: flex;
flex: 250px;
flex-direction: column;
margin: 0 5px;
& > strong {
display: block;
margin: 0 0 10px -5px;
font-weight: 500;
font-size: 14px;
line-height: 18px;
color: $secondary-text-color;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
.account-card {
flex: 1 1 auto;
}
}
.report-status,
.account-status {
display: flex;
margin-bottom: 10px;
.activity-stream {
flex: 2 0 0;
margin-right: 20px;
max-width: calc(100% - 60px);
.entry {
border-radius: 4px;
}
}
}
.report-status__actions,
.account-status__actions {
flex: 0 0 auto;
display: flex;
flex-direction: column;
.icon-button {
font-size: 24px;
width: 24px;
text-align: center;
margin-bottom: 10px;
}
}
.simple_form.new_report_note,
.simple_form.new_account_moderation_note {
max-width: 100%;
}
.batch-form-box {
display: flex;
flex-wrap: wrap;
margin-bottom: 5px;
#form_status_batch_action {
margin: 0 5px 5px 0;
font-size: 14px;
}
input.button {
margin: 0 5px 5px 0;
}
.media-spoiler-toggle-buttons {
margin-left: auto;
.button {
overflow: visible;
margin: 0 0 5px 5px;
float: right;
}
}
}
.back-link {
margin-bottom: 10px;
font-size: 14px;
a {
color: $highlight-text-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.spacer {
flex: 1 1 auto;
}
.log-entry {
margin-bottom: 20px;
line-height: 20px;
&__header {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 10px;
background: $ui-base-color;
color: $darker-text-color;
border-radius: 4px 4px 0 0;
font-size: 14px;
position: relative;
}
&__avatar {
margin-right: 10px;
.avatar {
display: block;
margin: 0;
border-radius: 50%;
width: 40px;
height: 40px;
}
}
&__content {
max-width: calc(100% - 90px);
}
&__title {
word-wrap: break-word;
}
&__timestamp {
color: $dark-text-color;
}
&__extras {
background: lighten($ui-base-color, 6%);
border-radius: 0 0 4px 4px;
padding: 10px;
color: $darker-text-color;
font-family: $font-monospace, monospace;
font-size: 12px;
word-wrap: break-word;
min-height: 20px;
}
&__icon {
font-size: 28px;
margin-right: 10px;
color: $dark-text-color;
}
&__icon__overlay {
position: absolute;
top: 10px;
right: 10px;
width: 10px;
height: 10px;
border-radius: 50%;
&.positive {
background: $success-green;
}
&.negative {
background: lighten($error-red, 12%);
}
&.neutral {
background: $ui-highlight-color;
}
}
a,
.username,
.target {
color: $secondary-text-color;
text-decoration: none;
font-weight: 500;
}
.diff-old {
color: lighten($error-red, 12%);
}
.diff-neutral {
color: $secondary-text-color;
}
.diff-new {
color: $success-green;
}
}
a.name-tag,
.name-tag,
a.inline-name-tag,
.inline-name-tag {
text-decoration: none;
color: $secondary-text-color;
.username {
font-weight: 500;
}
&.suspended {
.username {
text-decoration: line-through;
color: lighten($error-red, 12%);
}
.avatar {
filter: grayscale(100%);
opacity: 0.8;
}
}
}
a.name-tag,
.name-tag {
display: flex;
align-items: center;
.avatar {
display: block;
margin: 0;
margin-right: 5px;
border-radius: 50%;
}
&.suspended {
.avatar {
filter: grayscale(100%);
opacity: 0.8;
}
}
}
.speech-bubble {
margin-bottom: 20px;
border-left: 4px solid $ui-highlight-color;
&.positive {
border-left-color: $success-green;
}
&.negative {
border-left-color: lighten($error-red, 12%);
}
&.warning {
border-left-color: $gold-star;
}
&__bubble {
padding: 16px;
padding-left: 14px;
font-size: 15px;
line-height: 20px;
border-radius: 4px 4px 4px 0;
position: relative;
font-weight: 500;
a {
color: $darker-text-color;
}
}
&__owner {
padding: 8px;
padding-left: 12px;
}
time {
color: $dark-text-color;
}
}
.report-card {
background: $ui-base-color;
border-radius: 4px;
margin-bottom: 20px;
&__profile {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
.account {
padding: 0;
border: 0;
&__avatar-wrapper {
margin-left: 0;
}
}
&__stats {
flex: 0 0 auto;
font-weight: 500;
color: $darker-text-color;
text-transform: uppercase;
text-align: right;
a {
color: inherit;
text-decoration: none;
&:focus,
&:hover,
&:active {
color: lighten($darker-text-color, 8%);
}
}
.red {
color: $error-value-color;
}
}
}
&__summary {
&__item {
display: flex;
justify-content: flex-start;
border-top: 1px solid darken($ui-base-color, 4%);
&:hover {
background: lighten($ui-base-color, 2%);
}
&__reported-by,
&__assigned {
padding: 15px;
flex: 0 0 auto;
box-sizing: border-box;
width: 150px;
color: $darker-text-color;
&,
.username {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__content {
flex: 1 1 auto;
max-width: calc(100% - 300px);
&__icon {
color: $dark-text-color;
margin-right: 4px;
font-weight: 500;
}
}
&__content a {
display: block;
box-sizing: border-box;
width: 100%;
padding: 15px;
text-decoration: none;
color: $darker-text-color;
}
}
}
}
.one-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ellipsized-ip {
display: inline-block;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}

View File

@ -0,0 +1,164 @@
@function hex-color($color) {
@if type-of($color) == 'color' {
$color: str-slice(ie-hex-str($color), 4);
}
@return '%23' + unquote($color);
}
body {
font-family: $font-sans-serif, sans-serif;
background: darken($ui-base-color, 7%);
font-size: 13px;
line-height: 18px;
font-weight: 400;
color: $primary-text-color;
text-rendering: optimizelegibility;
font-feature-settings: "kern";
text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-tap-highlight-color: transparent;
&.system-font {
// system-ui => standard property (Chrome/Android WebView 56+, Opera 43+, Safari 11+)
// -apple-system => Safari <11 specific
// BlinkMacSystemFont => Chrome <56 on macOS specific
// Segoe UI => Windows 7/8/10
// Oxygen => KDE
// Ubuntu => Unity/Ubuntu
// Cantarell => GNOME
// Fira Sans => Firefox OS
// Droid Sans => Older Androids (<4.0)
// Helvetica Neue => Older macOS <10.11
// $font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0)
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", $font-sans-serif, sans-serif;
}
&.app-body {
padding: 0;
&.layout-single-column {
height: auto;
min-height: 100vh;
overflow-y: scroll;
}
&.layout-multiple-columns {
position: absolute;
width: 100%;
height: 100%;
}
&.with-modals--active {
overflow-y: hidden;
}
}
&.lighter {
background: $ui-base-color;
}
&.with-modals {
overflow-x: hidden;
overflow-y: scroll;
&--active {
overflow-y: hidden;
}
}
&.player {
text-align: center;
}
&.embed {
background: lighten($ui-base-color, 4%);
margin: 0;
padding-bottom: 0;
.container {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
}
&.admin {
background: darken($ui-base-color, 4%);
position: fixed;
width: 100%;
height: 100%;
padding: 0;
}
&.error {
position: absolute;
text-align: center;
color: $darker-text-color;
background: $ui-base-color;
width: 100%;
height: 100%;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
.dialog {
vertical-align: middle;
margin: 20px;
&__illustration {
img {
display: block;
max-width: 470px;
width: 100%;
height: auto;
margin-top: -120px;
}
}
h1 {
font-size: 20px;
line-height: 28px;
font-weight: 400;
}
}
}
}
button {
font-family: inherit;
cursor: pointer;
&:focus {
outline: none;
}
}
.app-holder {
&,
& > div {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
outline: 0 !important;
}
}
.layout-single-column .app-holder {
&,
& > div {
min-height: 100vh;
}
}
.layout-multiple-columns .app-holder {
&,
& > div {
height: 100%;
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,34 @@
.compact-header {
h1 {
font-size: 24px;
line-height: 28px;
color: $darker-text-color;
font-weight: 500;
margin-bottom: 20px;
padding: 0 10px;
word-wrap: break-word;
@media screen and (max-width: 740px) {
text-align: center;
padding: 20px 10px 0;
}
a {
color: inherit;
text-decoration: none;
}
small {
font-weight: 400;
color: $secondary-text-color;
}
img {
display: inline-block;
margin-bottom: -5px;
margin-right: 15px;
width: 36px;
height: 36px;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,815 @@
.container-alt {
width: 700px;
margin: 0 auto;
margin-top: 40px;
@media screen and (max-width: 740px) {
width: 100%;
margin: 0;
}
}
.logo-container {
margin: 100px auto 50px;
@media screen and (max-width: 500px) {
margin: 40px auto 0;
}
h1 {
display: flex;
justify-content: center;
align-items: center;
svg {
fill: $primary-text-color;
height: 42px;
margin-right: 10px;
}
a {
display: flex;
justify-content: center;
align-items: center;
color: $primary-text-color;
text-decoration: none;
outline: 0;
padding: 12px 16px;
line-height: 32px;
font-family: $font-display, sans-serif;
font-weight: 500;
font-size: 14px;
}
}
}
.compose-standalone {
.compose-form {
width: 400px;
margin: 0 auto;
padding: 20px 0;
margin-top: 40px;
box-sizing: border-box;
@media screen and (max-width: 400px) {
width: 100%;
margin-top: 0;
padding: 20px;
}
}
}
.account-header {
width: 400px;
margin: 0 auto;
display: flex;
font-size: 13px;
line-height: 18px;
box-sizing: border-box;
padding: 20px 0;
padding-bottom: 0;
margin-bottom: -30px;
margin-top: 40px;
@media screen and (max-width: 440px) {
width: 100%;
margin: 0;
margin-bottom: 10px;
padding: 20px;
padding-bottom: 0;
}
.avatar {
width: 40px;
height: 40px;
margin-right: 8px;
img {
width: 100%;
height: 100%;
display: block;
margin: 0;
border-radius: 4px;
}
}
.name {
flex: 1 1 auto;
color: $secondary-text-color;
width: calc(100% - 88px);
.username {
display: block;
font-weight: 500;
text-overflow: ellipsis;
overflow: hidden;
}
}
.logout-link {
display: block;
font-size: 32px;
line-height: 40px;
margin-left: 8px;
}
}
.grid-3 {
display: grid;
grid-gap: 10px;
grid-template-columns: 3fr 1fr;
grid-auto-columns: 25%;
grid-auto-rows: max-content;
.column-0 {
grid-column: 1 / 3;
grid-row: 1;
}
.column-1 {
grid-column: 1;
grid-row: 2;
}
.column-2 {
grid-column: 2;
grid-row: 2;
}
.column-3 {
grid-column: 1 / 3;
grid-row: 3;
}
.landing-page__call-to-action {
min-height: 100%;
}
.flash-message {
margin-bottom: 10px;
}
@media screen and (max-width: 738px) {
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
.landing-page__call-to-action {
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.row__information-board {
width: 100%;
justify-content: center;
align-items: center;
}
.row__mascot {
display: none;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
grid-gap: 0;
grid-template-columns: minmax(0, 100%);
.column-0 {
grid-column: 1;
}
.column-1 {
grid-column: 1;
grid-row: 3;
}
.column-2 {
grid-column: 1;
grid-row: 2;
}
.column-3 {
grid-column: 1;
grid-row: 4;
}
}
}
.public-layout {
@media screen and (max-width: $no-gap-breakpoint) {
padding-top: 48px;
}
.container {
max-width: 960px;
@media screen and (max-width: $no-gap-breakpoint) {
padding: 0;
}
}
.header {
background: lighten($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
height: 48px;
margin: 10px 0;
display: flex;
align-items: stretch;
justify-content: center;
flex-wrap: nowrap;
overflow: hidden;
@media screen and (max-width: $no-gap-breakpoint) {
position: fixed;
width: 100%;
top: 0;
left: 0;
margin: 0;
border-radius: 0;
box-shadow: none;
z-index: 110;
}
& > div {
flex: 1 1 33.3%;
min-height: 1px;
}
.nav-left {
display: flex;
align-items: stretch;
justify-content: flex-start;
flex-wrap: nowrap;
}
.nav-center {
display: flex;
align-items: stretch;
justify-content: center;
flex-wrap: nowrap;
}
.nav-right {
display: flex;
align-items: stretch;
justify-content: flex-end;
flex-wrap: nowrap;
}
.brand {
display: block;
padding: 15px;
svg {
display: block;
height: 18px;
width: auto;
position: relative;
bottom: -2px;
fill: $primary-text-color;
@media screen and (max-width: $no-gap-breakpoint) {
height: 20px;
}
}
&:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 12%);
}
}
.nav-link {
display: flex;
align-items: center;
padding: 0 1rem;
font-size: 12px;
font-weight: 500;
text-decoration: none;
color: $darker-text-color;
white-space: nowrap;
text-align: center;
&:hover,
&:focus,
&:active {
text-decoration: underline;
color: $primary-text-color;
}
@media screen and (max-width: 550px) {
&.optional {
display: none;
}
}
}
.nav-button {
background: lighten($ui-base-color, 16%);
margin: 8px;
margin-left: 0;
border-radius: 4px;
&:hover,
&:focus,
&:active {
text-decoration: none;
background: lighten($ui-base-color, 20%);
}
}
}
$no-columns-breakpoint: 600px;
.grid {
display: grid;
grid-gap: 10px;
grid-template-columns: minmax(300px, 3fr) minmax(298px, 1fr);
grid-auto-columns: 25%;
grid-auto-rows: max-content;
.column-0 {
grid-row: 1;
grid-column: 1;
}
.column-1 {
grid-row: 1;
grid-column: 2;
}
@media screen and (max-width: $no-columns-breakpoint) {
grid-template-columns: 100%;
grid-gap: 0;
.column-1 {
display: none;
}
}
}
.public-account-header {
overflow: hidden;
margin-bottom: 10px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
&.inactive {
opacity: 0.5;
.public-account-header__image,
.avatar {
filter: grayscale(100%);
}
.logo-button {
background-color: $secondary-text-color;
}
}
&__image {
border-radius: 4px 4px 0 0;
overflow: hidden;
height: 300px;
position: relative;
background: darken($ui-base-color, 12%);
&::after {
content: "";
display: block;
position: absolute;
width: 100%;
height: 100%;
box-shadow: inset 0 -1px 1px 1px rgba($base-shadow-color, 0.15);
top: 0;
left: 0;
}
img {
object-fit: cover;
display: block;
width: 100%;
height: 100%;
margin: 0;
border-radius: 4px 4px 0 0;
}
@media screen and (max-width: 600px) {
height: 200px;
}
}
&--no-bar {
margin-bottom: 0;
.public-account-header__image,
.public-account-header__image img {
border-radius: 4px;
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 0;
}
}
}
@media screen and (max-width: $no-gap-breakpoint) {
margin-bottom: 0;
box-shadow: none;
&__image::after {
display: none;
}
&__image,
&__image img {
border-radius: 0;
}
}
&__bar {
position: relative;
margin-top: -80px;
display: flex;
justify-content: flex-start;
&::before {
content: "";
display: block;
background: lighten($ui-base-color, 4%);
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60px;
border-radius: 0 0 4px 4px;
z-index: -1;
}
.avatar {
display: block;
width: 120px;
height: 120px;
padding-left: 20px - 4px;
flex: 0 0 auto;
img {
display: block;
width: 100%;
height: 100%;
margin: 0;
border-radius: 50%;
border: 4px solid lighten($ui-base-color, 4%);
background: darken($ui-base-color, 8%);
}
}
@media screen and (max-width: 600px) {
margin-top: 0;
background: lighten($ui-base-color, 4%);
border-radius: 0 0 4px 4px;
padding: 5px;
&::before {
display: none;
}
.avatar {
width: 48px;
height: 48px;
padding: 7px 0;
padding-left: 10px;
img {
border: 0;
border-radius: 4px;
}
@media screen and (max-width: 360px) {
display: none;
}
}
}
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 0;
}
@media screen and (max-width: $no-columns-breakpoint) {
flex-wrap: wrap;
}
}
&__tabs {
flex: 1 1 auto;
margin-left: 20px;
&__name {
padding-top: 20px;
padding-bottom: 8px;
h1 {
font-size: 20px;
line-height: 18px * 1.5;
color: $primary-text-color;
font-weight: 500;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-shadow: 1px 1px 1px $base-shadow-color;
small {
display: block;
font-size: 14px;
color: $primary-text-color;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
@media screen and (max-width: 600px) {
margin-left: 15px;
display: flex;
justify-content: space-between;
align-items: center;
&__name {
padding-top: 0;
padding-bottom: 0;
h1 {
font-size: 16px;
line-height: 24px;
text-shadow: none;
small {
color: $darker-text-color;
}
}
}
}
&__tabs {
display: flex;
justify-content: flex-start;
align-items: stretch;
height: 58px;
.details-counters {
display: flex;
flex-direction: row;
min-width: 300px;
}
@media screen and (max-width: $no-columns-breakpoint) {
.details-counters {
display: none;
}
}
.counter {
width: 33.3%;
box-sizing: border-box;
flex: 0 0 auto;
color: $darker-text-color;
padding: 10px;
border-right: 1px solid lighten($ui-base-color, 4%);
cursor: default;
text-align: center;
position: relative;
a {
display: block;
}
&:last-child {
border-right: 0;
}
&::after {
display: block;
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
border-bottom: 4px solid $ui-primary-color;
opacity: 0.5;
transition: all 400ms ease;
}
&.active {
&::after {
border-bottom: 4px solid $highlight-text-color;
opacity: 1;
}
&.inactive::after {
border-bottom-color: $secondary-text-color;
}
}
&:hover {
&::after {
opacity: 1;
transition-duration: 100ms;
}
}
a {
text-decoration: none;
color: inherit;
}
.counter-label {
font-size: 12px;
display: block;
}
.counter-number {
font-weight: 500;
font-size: 18px;
margin-bottom: 5px;
color: $primary-text-color;
font-family: $font-display, sans-serif;
}
}
.spacer {
flex: 1 1 auto;
height: 1px;
}
&__buttons {
padding: 7px 8px;
}
}
}
&__extra {
display: none;
margin-top: 4px;
.public-account-bio {
border-radius: 0;
box-shadow: none;
background: transparent;
margin: 0 -5px;
.account__header__fields {
border-top: 1px solid lighten($ui-base-color, 12%);
}
.roles {
display: none;
}
}
&__links {
margin-top: -15px;
font-size: 14px;
color: $darker-text-color;
a {
display: inline-block;
color: $darker-text-color;
text-decoration: none;
padding: 15px;
font-weight: 500;
strong {
font-weight: 700;
color: $primary-text-color;
}
}
}
@media screen and (max-width: $no-columns-breakpoint) {
display: block;
flex: 100%;
}
}
}
.account__section-headline {
border-radius: 4px 4px 0 0;
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 0;
}
}
.detailed-status__meta {
margin-top: 25px;
}
.public-account-bio {
background: lighten($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
@media screen and (max-width: $no-gap-breakpoint) {
box-shadow: none;
margin-bottom: 0;
border-radius: 0;
}
.account__header__fields {
margin: 0;
border-top: 0;
a {
color: lighten($ui-highlight-color, 8%);
}
dl:first-child .verified {
border-radius: 0 4px 0 0;
}
.verified a {
color: $valid-value-color;
}
}
.account__header__content {
padding: 20px;
padding-bottom: 0;
color: $primary-text-color;
}
&__extra,
.roles {
padding: 20px;
font-size: 14px;
color: $darker-text-color;
}
.roles {
padding-bottom: 0;
}
}
.static-icon-button {
color: $action-button-color;
font-size: 18px;
& > span {
font-size: 14px;
font-weight: 500;
}
}
.card-grid {
display: flex;
flex-wrap: wrap;
min-width: 100%;
margin: 0 -5px;
& > div {
box-sizing: border-box;
flex: 1 0 auto;
width: 300px;
padding: 0 5px;
margin-bottom: 10px;
max-width: 33.333%;
@media screen and (max-width: 900px) {
max-width: 50%;
}
@media screen and (max-width: 600px) {
max-width: 100%;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
margin: 0;
border-top: 1px solid lighten($ui-base-color, 8%);
& > div {
width: 100%;
padding: 0;
margin-bottom: 0;
border-bottom: 1px solid lighten($ui-base-color, 8%);
&:last-child {
border-bottom: 0;
}
.card__bar {
background: $ui-base-color;
&:hover,
&:active,
&:focus {
background: lighten($ui-base-color, 4%);
}
}
}
}
}
}

View File

@ -0,0 +1,76 @@
.dashboard__counters {
display: flex;
flex-wrap: wrap;
margin: 0 -5px;
margin-bottom: 20px;
& > div {
box-sizing: border-box;
flex: 0 0 33.333%;
padding: 0 5px;
margin-bottom: 10px;
& > div,
& > a {
padding: 20px;
background: lighten($ui-base-color, 4%);
border-radius: 4px;
}
& > a {
text-decoration: none;
color: inherit;
display: block;
&:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 8%);
}
}
}
&__num,
&__text {
text-align: center;
font-weight: 500;
font-size: 24px;
line-height: 21px;
color: $primary-text-color;
font-family: $font-display, sans-serif;
margin-bottom: 20px;
line-height: 30px;
}
&__text {
font-size: 18px;
}
&__label {
font-size: 14px;
color: $darker-text-color;
text-align: center;
font-weight: 500;
}
}
.dashboard__widgets {
display: flex;
flex-wrap: wrap;
margin: 0 -5px;
& > div {
flex: 0 0 33.333%;
margin-bottom: 20px;
& > div {
padding: 0 5px;
}
}
a:not(.name-tag) {
color: $ui-secondary-color;
font-weight: 500;
text-decoration: none;
}
}

View File

@ -0,0 +1,204 @@
.emoji-mart {
font-size: 13px;
display: inline-block;
color: $inverted-text-color;
&,
* {
box-sizing: border-box;
line-height: 1.15;
}
.emoji-mart-emoji {
padding: 6px;
}
}
.emoji-mart-bar {
border: 0 solid darken($ui-secondary-color, 8%);
&:first-child {
border-bottom-width: 1px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
background: $ui-secondary-color;
}
&:last-child {
border-top-width: 1px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
display: none;
}
}
.emoji-mart-anchors {
display: flex;
justify-content: space-between;
padding: 0 6px;
color: $lighter-text-color;
line-height: 0;
}
.emoji-mart-anchor {
position: relative;
flex: 1;
text-align: center;
padding: 12px 4px;
overflow: hidden;
transition: color .1s ease-out;
cursor: pointer;
&:hover {
color: darken($lighter-text-color, 4%);
}
}
.emoji-mart-anchor-selected {
color: $highlight-text-color;
&:hover {
color: darken($highlight-text-color, 4%);
}
.emoji-mart-anchor-bar {
bottom: -1px;
}
}
.emoji-mart-anchor-bar {
position: absolute;
bottom: -5px;
left: 0;
width: 100%;
height: 4px;
background-color: $highlight-text-color;
}
.emoji-mart-anchors {
i {
display: inline-block;
width: 100%;
max-width: 22px;
}
svg {
fill: currentColor;
max-height: 18px;
}
}
.emoji-mart-scroll {
overflow-y: scroll;
height: 270px;
max-height: 35vh;
padding: 0 6px 6px;
background: $simple-background-color;
will-change: transform;
&::-webkit-scrollbar-track:hover,
&::-webkit-scrollbar-track:active {
background-color: rgba($base-overlay-background, 0.3);
}
}
.emoji-mart-search {
padding: 10px;
padding-right: 45px;
background: $simple-background-color;
input {
font-size: 14px;
font-weight: 400;
padding: 7px 9px;
font-family: inherit;
display: block;
width: 100%;
background: rgba($ui-secondary-color, 0.3);
color: $inverted-text-color;
border: 1px solid $ui-secondary-color;
border-radius: 4px;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
}
}
.emoji-mart-category .emoji-mart-emoji {
cursor: pointer;
span {
z-index: 1;
position: relative;
text-align: center;
}
&:hover::before {
z-index: 0;
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba($ui-secondary-color, 0.7);
border-radius: 100%;
}
}
.emoji-mart-category-label {
z-index: 2;
position: relative;
position: -webkit-sticky;
position: sticky;
top: 0;
span {
display: block;
width: 100%;
font-weight: 500;
padding: 5px 6px;
background: $simple-background-color;
}
}
.emoji-mart-emoji {
position: relative;
display: inline-block;
font-size: 0;
span {
width: 22px;
height: 22px;
}
}
.emoji-mart-no-results {
font-size: 14px;
text-align: center;
padding-top: 70px;
color: $light-text-color;
.emoji-mart-category-label {
display: none;
}
.emoji-mart-no-results-label {
margin-top: .2em;
}
.emoji-mart-emoji:hover::before {
content: none;
}
}
.emoji-mart-preview {
display: none;
}

View File

@ -0,0 +1,164 @@
.public-layout {
.footer {
text-align: left;
padding-top: 20px;
padding-bottom: 60px;
font-size: 12px;
color: lighten($ui-base-color, 34%);
@media screen and (max-width: $no-gap-breakpoint) {
padding-left: 20px;
padding-right: 20px;
}
.grid {
display: grid;
grid-gap: 10px;
grid-template-columns: 1fr 1fr 2fr 1fr 1fr;
.column-0 {
grid-column: 1;
grid-row: 1;
min-width: 0;
}
.column-1 {
grid-column: 2;
grid-row: 1;
min-width: 0;
}
.column-2 {
grid-column: 3;
grid-row: 1;
min-width: 0;
text-align: center;
h4 a {
color: lighten($ui-base-color, 34%);
}
}
.column-3 {
grid-column: 4;
grid-row: 1;
min-width: 0;
}
.column-4 {
grid-column: 5;
grid-row: 1;
min-width: 0;
}
@media screen and (max-width: 690px) {
grid-template-columns: 1fr 2fr 1fr;
.column-0,
.column-1 {
grid-column: 1;
}
.column-1 {
grid-row: 2;
}
.column-2 {
grid-column: 2;
}
.column-3,
.column-4 {
grid-column: 3;
}
.column-4 {
grid-row: 2;
}
}
@media screen and (max-width: 600px) {
.column-1 {
display: block;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
.column-0,
.column-1,
.column-3,
.column-4 {
display: none;
}
}
}
h4 {
text-transform: uppercase;
font-weight: 700;
margin-bottom: 8px;
color: $darker-text-color;
a {
color: inherit;
text-decoration: none;
}
}
ul a {
text-decoration: none;
color: lighten($ui-base-color, 34%);
&:hover,
&:active,
&:focus {
text-decoration: underline;
}
}
.brand {
svg {
display: block;
height: 36px;
width: auto;
margin: 0 auto;
fill: lighten($ui-base-color, 34%);
}
&:hover,
&:focus,
&:active {
svg path {
fill: lighten($ui-base-color, 38%);
}
}
}
}
}
.getting-started__footer {
display: block;
position: fixed;
bottom: 0;
left: 1em;
width: 32vw;
z-index: 10;
text-align: right;
ul {
list-style-type: none;
}
li {
margin: 0.15rem 0;
display: inline-block
}
i {
margin: 0.5ch;
}
a {
margin-left: 2ch;
}
}

View File

@ -0,0 +1,971 @@
$no-columns-breakpoint: 600px;
table {
thead {
th {
font-weight: 800;
background: $ui-highlight-color;
}
}
td, th {
padding: 1rem;
}
a {
@extend .text-btn
}
}
.table-responsive {
width: 100%;
}
.table-striped {
margin: 1rem 0;
tr {
&:odd {
background: $ui-base-lighter-color;
}
}
}
.group-form {
}
code {
font-family: $font-monospace, monospace;
font-weight: 400;
}
.form-container {
max-width: 400px;
padding: 20px;
margin: 0 auto;
}
.simple_form {
.input {
margin-bottom: 15px;
overflow: hidden;
&.hidden {
margin: 0;
}
&.radio_buttons {
.radio {
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
}
.radio > label {
position: relative;
padding-left: 28px;
input {
position: absolute;
top: -2px;
left: 0;
}
}
}
&.boolean {
position: relative;
margin-bottom: 0;
.label_input > label {
font-family: inherit;
font-size: 14px;
padding-top: 5px;
color: $primary-text-color;
display: block;
width: auto;
}
.label_input,
.hint {
padding-left: 28px;
}
.label_input__wrapper {
position: static;
}
label.checkbox {
position: absolute;
top: 2px;
left: 0;
}
label a {
color: $highlight-text-color;
text-decoration: underline;
&:hover,
&:active,
&:focus {
text-decoration: none;
}
}
.recommended {
position: absolute;
margin: 0 4px;
margin-top: -2px;
background: $ui-highlight-color;
}
}
}
.row {
display: flex;
margin: 0 -5px;
.input {
box-sizing: border-box;
flex: 1 1 auto;
width: 50%;
padding: 0 5px;
}
}
.hint {
color: $darker-text-color;
a {
color: $highlight-text-color;
}
code {
border-radius: 3px;
padding: 0.2em 0.4em;
background: darken($ui-base-color, 12%);
}
}
span.hint {
display: block;
font-size: 12px;
margin-top: 4px;
}
p.hint {
margin-bottom: 15px;
color: $darker-text-color;
&.subtle-hint {
text-align: center;
font-size: 12px;
line-height: 18px;
margin-top: 15px;
margin-bottom: 0;
}
}
.card {
margin-bottom: 15px;
}
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
.input.with_floating_label {
.label_input {
display: flex;
& > label {
font-family: inherit;
font-size: 14px;
color: $primary-text-color;
font-weight: 500;
min-width: 150px;
flex: 0 0 auto;
}
input,
select {
flex: 1 1 auto;
}
}
&.select .hint {
margin-top: 6px;
margin-left: 150px;
}
}
.input.with_label {
.label_input > label {
font-family: inherit;
font-size: 14px;
color: $primary-text-color;
display: block;
margin-bottom: 8px;
word-wrap: break-word;
font-weight: 500;
}
.hint {
margin-top: 6px;
}
ul {
flex: 390px;
}
}
.input.with_block_label {
max-width: none;
& > label {
font-family: inherit;
font-size: 16px;
color: $primary-text-color;
display: block;
font-weight: 500;
padding-top: 5px;
}
.hint {
margin-bottom: 15px;
}
ul {
columns: 2;
}
}
.required abbr {
text-decoration: none;
color: lighten($error-value-color, 12%);
}
.fields-group {
margin-bottom: 25px;
.input:last-child {
margin-bottom: 0;
}
}
.fields-row {
display: flex;
margin: 0 -10px;
padding-top: 5px;
margin-bottom: 25px;
.input {
max-width: none;
}
&__column {
box-sizing: border-box;
padding: 0 10px;
flex: 1 1 auto;
min-height: 1px;
&-6 {
max-width: 50%;
}
}
.fields-group:last-child,
.fields-row__column.fields-group {
margin-bottom: 0;
}
@media screen and (max-width: $no-columns-breakpoint) {
display: block;
margin-bottom: 0;
&__column {
max-width: none;
}
.fields-group:last-child,
.fields-row__column.fields-group,
.fields-row__column {
margin-bottom: 25px;
}
}
}
.input.radio_buttons .radio label {
margin-bottom: 5px;
font-family: inherit;
font-size: 14px;
color: $primary-text-color;
display: block;
width: auto;
}
.check_boxes {
.checkbox {
label {
font-family: inherit;
font-size: 14px;
color: $primary-text-color;
display: inline-block;
width: auto;
position: relative;
padding-top: 5px;
padding-left: 25px;
flex: 1 1 auto;
}
input[type=checkbox] {
position: absolute;
left: 0;
top: 5px;
margin: 0;
}
}
}
.input.static .label_input__wrapper {
font-size: 16px;
padding: 10px;
border: 1px solid $dark-text-color;
border-radius: 4px;
}
input[type=text],
input[type=number],
input[type=email],
input[type=password],
textarea {
box-sizing: border-box;
font-size: 16px;
color: $primary-text-color;
display: block;
width: 100%;
outline: 0;
font-family: inherit;
resize: vertical;
background: darken($ui-base-color, 10%);
border: 1px solid darken($ui-base-color, 14%);
border-radius: 4px;
padding: 10px;
&:invalid {
box-shadow: none;
}
&:focus:invalid:not(:placeholder-shown) {
border-color: lighten($error-red, 12%);
}
&:required:valid {
border-color: $valid-value-color;
}
&:hover {
border-color: darken($ui-base-color, 20%);
}
&:active,
&:focus {
border-color: $highlight-text-color;
background: darken($ui-base-color, 8%);
}
}
.input.field_with_errors {
label {
color: lighten($error-red, 12%);
}
input[type=text],
input[type=number],
input[type=email],
input[type=password],
textarea,
select {
border-color: lighten($error-red, 12%);
}
.error {
display: block;
font-weight: 500;
color: lighten($error-red, 12%);
margin-top: 4px;
}
}
.input.disabled {
opacity: 0.5;
}
.actions {
margin-top: 30px;
display: flex;
&.actions--top {
margin-top: 0;
margin-bottom: 30px;
}
}
button,
.button,
.block-button {
display: block;
width: 100%;
border: 0;
border-radius: 4px;
background: $ui-highlight-color;
color: $primary-text-color;
font-size: 18px;
line-height: inherit;
height: auto;
padding: 10px;
text-transform: uppercase;
text-decoration: none;
text-align: center;
box-sizing: border-box;
cursor: pointer;
font-weight: 500;
outline: 0;
margin-bottom: 10px;
margin-right: 10px;
&:last-child {
margin-right: 0;
}
&:hover {
background-color: lighten($ui-highlight-color, 5%);
}
&:active,
&:focus {
background-color: darken($ui-highlight-color, 5%);
}
&:disabled:hover {
background-color: $ui-primary-color;
}
&.negative {
background: $error-value-color;
&:hover {
background-color: lighten($error-value-color, 5%);
}
&:active,
&:focus {
background-color: darken($error-value-color, 5%);
}
}
}
select {
appearance: none;
box-sizing: border-box;
font-size: 16px;
color: $primary-text-color;
display: block;
width: 100%;
outline: 0;
font-family: inherit;
resize: vertical;
background: darken($ui-base-color, 10%) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 12%))}'/></svg>") no-repeat right 8px center / auto 16px;
border: 1px solid darken($ui-base-color, 14%);
border-radius: 4px;
padding-left: 10px;
padding-right: 30px;
height: 41px;
}
h4 {
margin-bottom: 15px !important;
}
.label_input {
&__wrapper {
position: relative;
}
&__append {
position: absolute;
right: 3px;
top: 1px;
padding: 10px;
padding-bottom: 9px;
font-size: 16px;
color: $dark-text-color;
font-family: inherit;
pointer-events: none;
cursor: default;
max-width: 140px;
white-space: nowrap;
overflow: hidden;
&::after {
content: '';
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 1px;
width: 5px;
background-image: linear-gradient(to right, rgba(darken($ui-base-color, 10%), 0), darken($ui-base-color, 10%));
}
}
}
&__overlay-area {
position: relative;
&__overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: rgba($ui-base-color, 0.65);
backdrop-filter: blur(2px);
border-radius: 4px;
&__content {
text-align: center;
&.rich-formatting {
&,
p {
color: $primary-text-color;
}
}
}
}
}
}
.block-icon {
display: block;
margin: 0 auto;
margin-bottom: 10px;
font-size: 24px;
}
.flash-message {
background: lighten($ui-base-color, 8%);
color: $darker-text-color;
border-radius: 4px;
padding: 15px 10px;
margin-bottom: 30px;
text-align: center;
&.notice {
border: 1px solid rgba($valid-value-color, 0.5);
background: rgba($valid-value-color, 0.25);
color: $valid-value-color;
}
&.alert {
border: 1px solid rgba($error-value-color, 0.5);
background: rgba($error-value-color, 0.25);
color: $error-value-color;
}
a {
display: inline-block;
color: $darker-text-color;
text-decoration: none;
&:hover {
color: $primary-text-color;
text-decoration: underline;
}
}
p {
margin-bottom: 15px;
}
.oauth-code {
outline: 0;
box-sizing: border-box;
display: block;
width: 100%;
border: 0;
padding: 10px;
font-family: $font-monospace, monospace;
background: $ui-base-color;
color: $primary-text-color;
font-size: 14px;
margin: 0;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
}
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
@media screen and (max-width: 740px) and (min-width: 441px) {
margin-top: 40px;
}
}
.form-footer {
margin-top: 30px;
text-align: center;
a {
color: $darker-text-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.quick-nav {
list-style: none;
margin-bottom: 25px;
font-size: 14px;
li {
display: inline-block;
margin-right: 10px;
}
a {
color: $highlight-text-color;
text-transform: uppercase;
text-decoration: none;
font-weight: 700;
&:hover,
&:focus,
&:active {
color: lighten($highlight-text-color, 8%);
}
}
}
.oauth-prompt,
.follow-prompt {
margin-bottom: 30px;
color: $darker-text-color;
h2 {
font-size: 16px;
margin-bottom: 30px;
text-align: center;
}
strong {
color: $secondary-text-color;
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
@media screen and (max-width: 740px) and (min-width: 441px) {
margin-top: 40px;
}
}
.qr-wrapper {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
}
.qr-code {
flex: 0 0 auto;
background: $simple-background-color;
padding: 4px;
margin: 0 10px 20px 0;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
display: inline-block;
svg {
display: block;
margin: 0;
}
}
.qr-alternative {
margin-bottom: 20px;
color: $secondary-text-color;
flex: 150px;
samp {
display: block;
font-size: 14px;
}
}
.table-form {
p {
margin-bottom: 15px;
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
}
}
.simple_form,
.table-form {
.warning {
box-sizing: border-box;
background: rgba($error-value-color, 0.5);
color: $primary-text-color;
text-shadow: 1px 1px 0 rgba($base-shadow-color, 0.3);
box-shadow: 0 2px 6px rgba($base-shadow-color, 0.4);
border-radius: 4px;
padding: 10px;
margin-bottom: 15px;
a {
color: $primary-text-color;
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
strong {
font-weight: 600;
display: block;
margin-bottom: 5px;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
.fa {
font-weight: 400;
}
}
}
}
.action-pagination {
display: flex;
flex-wrap: wrap;
align-items: center;
.actions,
.pagination {
flex: 1 1 auto;
}
.actions {
padding: 30px 0;
padding-right: 20px;
flex: 0 0 auto;
}
}
.post-follow-actions {
text-align: center;
color: $darker-text-color;
div {
margin-bottom: 4px;
}
}
.alternative-login {
margin-top: 20px;
margin-bottom: 20px;
h4 {
font-size: 16px;
color: $primary-text-color;
text-align: center;
margin-bottom: 20px;
border: 0;
padding: 0;
}
.button {
display: block;
}
}
.scope-danger {
color: $warning-red;
}
.form_admin_settings_site_short_description,
.form_admin_settings_site_description,
.form_admin_settings_site_extended_description,
.form_admin_settings_site_terms,
.form_admin_settings_custom_css,
.form_admin_settings_closed_registrations_message {
textarea {
font-family: $font-monospace, monospace;
}
}
.input-copy {
background: darken($ui-base-color, 10%);
border: 1px solid darken($ui-base-color, 14%);
border-radius: 4px;
display: flex;
align-items: center;
padding-right: 4px;
position: relative;
top: 1px;
transition: border-color 300ms linear;
&__wrapper {
flex: 1 1 auto;
}
input[type=text] {
background: transparent;
border: 0;
padding: 10px;
font-size: 14px;
font-family: $font-monospace, monospace;
}
button {
flex: 0 0 auto;
margin: 4px;
text-transform: none;
font-weight: 400;
font-size: 14px;
padding: 7px 18px;
padding-bottom: 6px;
width: auto;
transition: background 300ms linear;
}
&.copied {
border-color: $valid-value-color;
transition: none;
button {
background: $valid-value-color;
transition: none;
}
}
}
.connection-prompt {
margin-bottom: 25px;
.fa-link {
background-color: darken($ui-base-color, 4%);
border-radius: 100%;
font-size: 24px;
padding: 10px;
}
&__column {
align-items: center;
display: flex;
flex: 1;
flex-direction: column;
flex-shrink: 1;
max-width: 50%;
&-sep {
align-self: center;
flex-grow: 0;
overflow: visible;
position: relative;
z-index: 1;
}
p {
word-break: break-word;
}
}
.account__avatar {
margin-bottom: 20px;
}
&__connection {
background-color: lighten($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
padding: 25px 10px;
position: relative;
text-align: center;
&::after {
background-color: darken($ui-base-color, 4%);
content: '';
display: block;
height: 100%;
left: 50%;
position: absolute;
top: 0;
width: 1px;
}
}
&__row {
align-items: flex-start;
display: flex;
flex-direction: row;
}
}
.compose-form__publish-button-wrapper {
width: 100%;
display: block;
}

View File

@ -0,0 +1,153 @@
.introduction {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
@media screen and (max-width: 920px) {
background: darken($ui-base-color, 8%);
display: block !important;
}
&__pager {
background: darken($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
overflow: hidden;
}
&__pager,
&__frame {
border-radius: 10px;
width: 50vw;
min-width: 920px;
@media screen and (max-width: 920px) {
min-width: 0;
width: 100%;
border-radius: 0;
box-shadow: none;
}
}
&__frame-wrapper {
opacity: 0;
transition: opacity 500ms linear;
&.active {
opacity: 1;
transition: opacity 50ms linear;
}
}
&__frame {
overflow: hidden;
}
&__illustration {
height: 50vh;
@media screen and (max-width: 630px) {
height: auto;
}
img {
object-fit: cover;
display: block;
margin: 0;
width: 100%;
height: 100%;
}
}
&__text {
border-top: 2px solid $ui-highlight-color;
&--columnized {
display: flex;
& > div {
flex: 1 1 33.33%;
text-align: center;
padding: 25px;
padding-bottom: 30px;
}
@media screen and (max-width: 630px) {
display: block;
padding: 15px 0;
padding-bottom: 20px;
& > div {
padding: 10px 25px;
}
}
}
h3 {
font-size: 24px;
line-height: 1.5;
font-weight: 700;
margin-bottom: 10px;
}
p {
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: $darker-text-color;
code {
display: inline-block;
background: darken($ui-base-color, 8%);
font-size: 15px;
border: 1px solid lighten($ui-base-color, 8%);
border-radius: 2px;
padding: 1px 3px;
}
}
&--centered {
padding: 25px;
padding-bottom: 30px;
text-align: center;
}
}
&__dots {
display: flex;
align-items: center;
justify-content: center;
padding: 25px;
@media screen and (max-width: 630px) {
display: none;
}
}
&__dot {
width: 14px;
height: 14px;
border-radius: 14px;
border: 1px solid $ui-highlight-color;
background: transparent;
margin: 0 3px;
cursor: pointer;
&:hover {
background: lighten($ui-base-color, 8%);
}
&.active {
cursor: default;
background: $ui-highlight-color;
}
}
&__action {
padding: 25px;
padding-top: 0;
display: flex;
align-items: center;
justify-content: center;
}
}

View File

@ -0,0 +1,19 @@
.no-list {
list-style: none;
li {
display: inline-block;
margin: 0 5px;
}
}
.recovery-codes {
list-style: none;
margin: 0 auto;
li {
font-size: 125%;
line-height: 1.5;
letter-spacing: 1px;
}
}

View File

@ -0,0 +1,134 @@
$messagingBoxWidth: 15em;
$messagingBoxHeight: 20em;
.fixed-box {
border: solid 1px white;
padding: 1em;
position: fixed;
bottom: 0;
}
.airmail-border {
border: 0.25em solid transparent;
border-image: 4 repeating-linear-gradient(-45deg, red 0, red 1em, white 0, white 2em,
#58a 0, #58a 3em, white 0, white 4em);
}
.status-direct,
.item-list .conversation {
@extend .airmail-border;
}
.messaging-box {
@extend .fixed-box;
right: 1em;
width: $messagingBoxWidth;
background: $ui-base-color;
.messager-textarea {
width: 100%;
}
}
.conversation {
.conversation__content {
padding-right: 0;
}
.conversation_reply,
.icon-button {
&:hover {
color: $ui-highlight-color;
background: mix($ui-base-color, $ui-secondary-color);
}
}
.conversation_reply,
.icon-button,
.status__action-bar-dropdown {
display: inline-block;
float: right;
width: 18em;
height: 3.2em;
text-align: center;
}
}
.conversations_list {
right: 1em;
position: absolute;
bottom: 0;
width: 100%;
padding: 0.5em;
background: gray;
}
.conversation-item {
@extend .fixed-box;
width: $messagingBoxWidth;
right: $messagingBoxWidth + 5em;
background: $ui-secondary-color;
&.has-new-message {
background: $ui-highlight-color;
color: $classic-primary-color;
}
}
.conversation_created-at {
margin-right: 1em;
}
.conversation_stream {
padding-top: 1em;
height: $messagingBoxHeight;
overflow: auto;
background: $ui-secondary-color;
.message {
-webkit-border-radius: 0.5rem;
-moz-border-radius: 0.5rem;
border-radius: 0.5rem;
margin-bottom: 0.5em;
padding: 0.5em 1em;
width: 80%;
}
.mine {
text-align: right;
background: $classic-primary-color;
float: right;
.arrow-down {
border-top-color: $classic-primary-color;
left: 1em;
}
}
.theirs {
text-align: left;
background: $ui-highlight-color;
float: left;
.arrow-down {
border-top-color: $ui-highlight-color;
right: 1em;
}
}
.arrow-down {
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 20px solid transparent;
border-top: 20px solid $classic-primary-color;
position: relative;
bottom: -1em;
}
}

View File

@ -0,0 +1,26 @@
.modal-layout {
background: $ui-base-color url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-lighter-color)}"/></svg>') repeat-x bottom fixed;
display: flex;
flex-direction: column;
height: 100vh;
padding: 0;
}
.modal-layout__mastodon {
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-end;
> * {
flex: 1;
max-height: 235px;
background: url('../images/elephant_ui_plane.svg') no-repeat left bottom / contain;
}
}
@media screen and (max-width: 600px) {
.account-header {
margin-top: 0;
}
}

View File

@ -0,0 +1,210 @@
.poll {
margin-top: 16px;
font-size: 14px;
li {
margin-bottom: 10px;
position: relative;
height: 18px + 12px;
}
&__chart {
position: absolute;
top: 0;
left: 0;
height: 100%;
display: inline-block;
border-radius: 4px;
background: darken($ui-primary-color, 14%);
&.leading {
background: $ui-highlight-color;
}
}
&__text {
position: relative;
display: inline-block;
padding: 6px 0;
line-height: 18px;
cursor: default;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
input[type=radio],
input[type=checkbox] {
display: none;
}
.autossugest-input {
flex: 1 1 auto;
}
input[type=text] {
display: block;
box-sizing: border-box;
width: 100%;
font-size: 14px;
color: $inverted-text-color;
outline: 0;
font-family: inherit;
background: $simple-background-color;
border: 1px solid darken($simple-background-color, 14%);
border-radius: 4px;
padding: 6px 10px;
&:focus {
border-color: $highlight-text-color;
}
}
&.selectable {
cursor: pointer;
}
&.editable {
display: flex;
align-items: center;
overflow: visible;
}
}
&__input {
display: inline-block;
position: relative;
border: 1px solid $ui-primary-color;
box-sizing: border-box;
width: 18px;
height: 18px;
flex: 0 0 auto;
margin-right: 10px;
top: -1px;
border-radius: 50%;
vertical-align: middle;
&.checkbox {
border-radius: 4px;
}
&.active {
border-color: $valid-value-color;
background: $valid-value-color;
}
}
&__number {
display: inline-block;
width: 36px;
font-weight: 700;
padding: 0 10px;
text-align: right;
}
&__footer {
padding-top: 6px;
padding-bottom: 5px;
color: $dark-text-color;
}
&__link {
display: inline;
background: transparent;
padding: 0;
margin: 0;
border: 0;
color: $dark-text-color;
text-decoration: underline;
font-size: inherit;
&:hover {
text-decoration: none;
}
&:active,
&:focus {
background-color: rgba($dark-text-color, .1);
}
}
.button {
height: 36px;
padding: 0 16px;
margin-right: 10px;
font-size: 14px;
}
}
.compose-form__poll-wrapper {
border-top: 1px solid darken($simple-background-color, 8%);
ul {
padding: 10px;
}
.poll__footer {
border-top: 1px solid darken($simple-background-color, 8%);
padding: 10px;
display: flex;
align-items: center;
button,
select {
flex: 1 1 50%;
}
}
.button.button-secondary {
font-size: 14px;
font-weight: 400;
padding: 6px 10px;
height: auto;
line-height: inherit;
color: $action-button-color;
border-color: $action-button-color;
margin-right: 5px;
}
li {
display: flex;
align-items: center;
.poll__text {
flex: 0 0 auto;
width: calc(100% - (23px + 6px));
margin-right: 6px;
}
}
select {
appearance: none;
box-sizing: border-box;
font-size: 14px;
color: $inverted-text-color;
display: inline-block;
width: auto;
outline: 0;
font-family: inherit;
background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(darken($simple-background-color, 14%))}'/></svg>") no-repeat right 8px center / auto 16px;
border: 1px solid darken($simple-background-color, 14%);
border-radius: 4px;
padding: 6px 10px;
padding-right: 30px;
}
.icon-button.disabled {
color: darken($simple-background-color, 14%);
}
}
.muted .poll {
color: $dark-text-color;
&__chart {
background: rgba(darken($ui-primary-color, 14%), 0.2);
&.leading {
background: rgba($ui-highlight-color, 0.2);
}
}
}

View File

@ -0,0 +1,95 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
html {
scrollbar-color: lighten($ui-base-color, 4%) rgba($base-overlay-background, 0.1);
}
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-thumb {
background: lighten($ui-base-color, 4%);
border: 0px none $base-border-color;
border-radius: 50px;
}
::-webkit-scrollbar-thumb:hover {
background: lighten($ui-base-color, 6%);
}
::-webkit-scrollbar-thumb:active {
background: lighten($ui-base-color, 4%);
}
::-webkit-scrollbar-track {
border: 0px none $base-border-color;
border-radius: 0;
background: rgba($base-overlay-background, 0.1);
}
::-webkit-scrollbar-track:hover {
background: $ui-base-color;
}
::-webkit-scrollbar-track:active {
background: $ui-base-color;
}
::-webkit-scrollbar-corner {
background: transparent;
}

View File

@ -0,0 +1,385 @@
body.rtl {
direction: rtl;
.column-header > button {
text-align: right;
padding-left: 0;
padding-right: 15px;
}
.landing-page__logo {
margin-right: 0;
margin-left: 20px;
}
.landing-page .features-list .features-list__row .visual {
margin-left: 0;
margin-right: 15px;
}
.column-link__icon,
.column-header__icon {
margin-right: 0;
margin-left: 5px;
}
.compose-form .compose-form__buttons-wrapper .character-counter__wrapper {
margin-right: 0;
margin-left: 4px;
}
.navigation-bar__profile {
margin-left: 0;
margin-right: 8px;
}
.search__input {
padding-right: 10px;
padding-left: 30px;
}
.search__icon .fa {
right: auto;
left: 10px;
}
.columns-area {
direction: rtl;
}
.column-header__buttons {
left: 0;
right: auto;
margin-left: 0;
margin-right: -15px;
}
.column-inline-form .icon-button {
margin-left: 0;
margin-right: 5px;
}
.column-header__links .text-btn {
margin-left: 10px;
margin-right: 0;
}
.account__avatar-wrapper {
float: right;
}
.column-header__back-button {
padding-left: 5px;
padding-right: 0;
}
.column-header__setting-arrows {
float: left;
}
.setting-toggle__label {
margin-left: 0;
margin-right: 8px;
}
.status__avatar {
left: auto;
right: 10px;
}
.status,
.activity-stream .status.light {
padding-left: 10px;
padding-right: 68px;
}
.status__info .status__display-name,
.activity-stream .status.light .status__display-name {
padding-left: 25px;
padding-right: 0;
}
.activity-stream .pre-header {
padding-right: 68px;
padding-left: 0;
}
.status__prepend {
margin-left: 0;
margin-right: 68px;
}
.status__prepend-icon-wrapper {
left: auto;
right: -26px;
}
.activity-stream .pre-header .pre-header__icon {
left: auto;
right: 42px;
}
.account__avatar-overlay-overlay {
right: auto;
left: 0;
}
.column-back-button--slim-button {
right: auto;
left: 0;
}
.status__relative-time,
.activity-stream .status.light .status__header .status__meta {
float: left;
}
.status__action-bar {
&__counter {
margin-right: 0;
margin-left: 11px;
.status__action-bar-button {
margin-right: 0;
margin-left: 4px;
}
}
}
.status__action-bar-button {
float: right;
margin-right: 0;
margin-left: 18px;
}
.status__action-bar-dropdown {
float: right;
}
.privacy-dropdown__dropdown {
margin-left: 0;
margin-right: 40px;
}
.privacy-dropdown__option__icon {
margin-left: 10px;
margin-right: 0;
}
.detailed-status__display-name .display-name {
text-align: right;
}
.detailed-status__display-avatar {
margin-right: 0;
margin-left: 10px;
float: right;
}
.detailed-status__favorites,
.detailed-status__reblogs {
margin-left: 0;
margin-right: 6px;
}
.fa-ul {
margin-left: 2.14285714em;
}
.fa-li {
left: auto;
right: -2.14285714em;
}
.admin-wrapper {
direction: rtl;
}
.admin-wrapper .sidebar ul a i.fa,
a.table-action-link i.fa {
margin-right: 0;
margin-left: 5px;
}
.simple_form .check_boxes .checkbox label {
padding-left: 0;
padding-right: 25px;
}
.simple_form .input.with_label.boolean label.checkbox {
padding-left: 25px;
padding-right: 0;
}
.simple_form .check_boxes .checkbox input[type="checkbox"],
.simple_form .input.boolean input[type="checkbox"] {
left: auto;
right: 0;
}
.simple_form .input.radio_buttons .radio {
left: auto;
right: 0;
}
.simple_form .input.radio_buttons .radio > label {
padding-right: 28px;
padding-left: 0;
}
.simple_form .input-with-append .input input {
padding-left: 142px;
padding-right: 0;
}
.simple_form .input.boolean label.checkbox {
left: auto;
right: 0;
}
.simple_form .input.boolean .label_input,
.simple_form .input.boolean .hint {
padding-left: 0;
padding-right: 28px;
}
.simple_form .label_input__append {
right: auto;
left: 3px;
&::after {
right: auto;
left: 0;
background-image: linear-gradient(to left, rgba(darken($ui-base-color, 10%), 0), darken($ui-base-color, 10%));
}
}
.simple_form select {
background: darken($ui-base-color, 10%) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 12%))}'/></svg>") no-repeat left 8px center / auto 16px;
}
.table th,
.table td {
text-align: right;
}
.filters .filter-subset {
margin-right: 0;
margin-left: 45px;
}
.landing-page .header-wrapper .mascot {
right: 60px;
left: auto;
}
.landing-page__call-to-action .row__information-board {
direction: rtl;
}
.landing-page .header .hero .floats .float-1 {
left: -120px;
right: auto;
}
.landing-page .header .hero .floats .float-2 {
left: 210px;
right: auto;
}
.landing-page .header .hero .floats .float-3 {
left: 110px;
right: auto;
}
.landing-page .header .links .brand img {
left: 0;
}
.landing-page .fa-external-link {
padding-right: 5px;
padding-left: 0 !important;
}
.landing-page .features #mastodon-timeline {
margin-right: 0;
margin-left: 30px;
}
@media screen and (min-width: 631px) {
.column,
.drawer {
padding-left: 5px;
padding-right: 5px;
&:first-child {
padding-left: 5px;
padding-right: 10px;
}
}
.columns-area > div {
.column,
.drawer {
padding-left: 5px;
padding-right: 5px;
}
}
}
.public-layout {
.header {
.nav-button {
margin-left: 8px;
margin-right: 0;
}
}
.public-account-header__tabs {
margin-left: 0;
margin-right: 20px;
}
}
.landing-page__information {
.account__display-name {
margin-right: 0;
margin-left: 5px;
}
.account__avatar-wrapper {
margin-left: 12px;
margin-right: 0;
}
}
.card__bar .display-name {
margin-left: 0;
margin-right: 15px;
text-align: right;
}
.fa-chevron-left::before {
content: "\F054";
}
.fa-chevron-right::before {
content: "\F053";
}
.column-back-button__icon {
margin-right: 0;
margin-left: 5px;
}
.column-header__setting-arrows .column-header__setting-btn:last-child {
padding-left: 0;
padding-right: 10px;
}
.simple_form .input.radio_buttons .radio > label input {
left: auto;
right: 0;
}
}

View File

@ -0,0 +1,163 @@
.activity-stream {
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
&--under-tabs {
border-radius: 0 0 4px 4px;
}
@media screen and (max-width: $no-gap-breakpoint) {
margin-bottom: 0;
border-radius: 0;
box-shadow: none;
}
&--headless {
border-radius: 0;
margin: 0;
box-shadow: none;
.detailed-status,
.status {
border-radius: 0 !important;
}
}
div[data-component] {
width: 100%;
}
.entry {
background: $ui-base-color;
.detailed-status,
.status,
.load-more {
animation: none;
}
&:last-child {
.detailed-status,
.status,
.load-more {
border-bottom: 0;
border-radius: 0 0 4px 4px;
}
}
&:first-child {
.detailed-status,
.status,
.load-more {
border-radius: 4px 4px 0 0;
}
&:last-child {
.detailed-status,
.status,
.load-more {
border-radius: 4px;
}
}
}
@media screen and (max-width: 740px) {
.detailed-status,
.status,
.load-more {
border-radius: 0 !important;
}
}
}
&--highlighted .entry {
background: lighten($ui-base-color, 8%);
}
}
.button.logo-button {
flex: 0 auto;
font-size: 14px;
background: $ui-highlight-color;
color: $primary-text-color;
text-transform: none;
line-height: 36px;
height: auto;
padding: 3px 15px;
border: 0;
svg {
width: 20px;
height: auto;
vertical-align: middle;
margin-right: 5px;
fill: $primary-text-color;
}
&:active,
&:focus,
&:hover {
background: lighten($ui-highlight-color, 10%);
}
&:disabled,
&.disabled {
&:active,
&:focus,
&:hover {
background: $ui-primary-color;
}
}
&.button--destructive {
&:active,
&:focus,
&:hover {
background: $error-red;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
svg {
display: none;
}
}
}
.embed,
.public-layout {
.detailed-status {
padding: 15px;
}
.status {
padding: 15px 15px 15px (48px + 15px * 2);
min-height: 48px + 2px;
&__avatar {
left: 15px;
top: 17px;
}
&__content {
padding-top: 5px;
}
&__prepend {
margin-left: 48px + 15px * 2;
padding-top: 15px;
}
&__prepend-icon-wrapper {
left: -32px;
}
.media-gallery,
&__action-bar,
.video-player {
margin-top: 10px;
}
}
}

View File

@ -0,0 +1,243 @@
.table {
width: 100%;
max-width: 100%;
border-spacing: 0;
border-collapse: collapse;
th,
td {
padding: 8px;
line-height: 18px;
vertical-align: top;
border-top: 1px solid $ui-base-color;
text-align: left;
background: darken($ui-base-color, 4%);
}
& > thead > tr > th {
vertical-align: bottom;
border-bottom: 2px solid $ui-base-color;
border-top: 0;
font-weight: 500;
}
& > tbody > tr > th {
font-weight: 500;
}
& > tbody > tr:nth-child(odd) > td,
& > tbody > tr:nth-child(odd) > th {
background: $ui-base-color;
}
a {
color: $highlight-text-color;
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
&.inline-table {
& > tbody > tr:nth-child(odd) {
& > td,
& > th {
background: transparent;
}
}
& > tbody > tr:first-child {
& > td,
& > th {
border-top: 0;
}
}
}
&.batch-table {
& > thead > tr > th {
background: $ui-base-color;
border-top: 1px solid darken($ui-base-color, 8%);
border-bottom: 1px solid darken($ui-base-color, 8%);
&:first-child {
border-radius: 4px 0 0;
border-left: 1px solid darken($ui-base-color, 8%);
}
&:last-child {
border-radius: 0 4px 0 0;
border-right: 1px solid darken($ui-base-color, 8%);
}
}
}
&--invites tbody td {
vertical-align: middle;
}
}
.table-wrapper {
overflow: auto;
margin-bottom: 20px;
}
samp {
font-family: $font-monospace, monospace;
}
button.table-action-link {
background: transparent;
border: 0;
font: inherit;
}
button.table-action-link,
a.table-action-link {
text-decoration: none;
display: inline-block;
margin-right: 5px;
padding: 0 10px;
color: $darker-text-color;
font-weight: 500;
&:hover {
color: $primary-text-color;
}
i.fa {
font-weight: 400;
margin-right: 5px;
}
&:first-child {
padding-left: 0;
}
}
.batch-table {
&__toolbar,
&__row {
display: flex;
&__select {
box-sizing: border-box;
padding: 8px 16px;
cursor: pointer;
min-height: 100%;
input {
margin-top: 8px;
}
&--aligned {
display: flex;
align-items: center;
input {
margin-top: 0;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
&__actions,
&__content {
padding: 8px 0;
padding-right: 16px;
flex: 1 1 auto;
}
}
&__toolbar {
border: 1px solid darken($ui-base-color, 8%);
background: $ui-base-color;
border-radius: 4px 0 0;
height: 47px;
align-items: center;
&__actions {
text-align: right;
padding-right: 16px - 5px;
}
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
&__row {
border: 1px solid darken($ui-base-color, 8%);
border-top: 0;
background: darken($ui-base-color, 4%);
@media screen and (max-width: $no-gap-breakpoint) {
&:first-child {
border-top: 1px solid darken($ui-base-color, 8%);
}
}
&:hover {
background: darken($ui-base-color, 2%);
}
&:nth-child(even) {
background: $ui-base-color;
&:hover {
background: lighten($ui-base-color, 2%);
}
}
&__content {
padding-top: 12px;
padding-bottom: 16px;
&--unpadded {
padding: 0;
}
}
}
.status__content {
padding-top: 0;
summary {
display: list-item;
}
strong {
font-weight: 700;
}
}
.nothing-here {
border: 1px solid darken($ui-base-color, 8%);
border-top: 0;
box-shadow: none;
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 1px solid darken($ui-base-color, 8%);
}
}
@media screen and (max-width: 870px) {
.accounts-table tbody td.optional {
display: none;
}
}
}

View File

@ -0,0 +1,54 @@
// Commonly used web colors
$black: #000000; // Black
$white: #ffffff; // White
$success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
// Values from the classic Mastodon UI
$classic-base-color: #282c37; // Midnight Express
$classic-primary-color: #9baec8; // Echo Blue
$classic-secondary-color: #d9e1e8; // Pattens Blue
$classic-highlight-color: #2b90d9; // Summer Sky
// Variables for defaults in UI
$base-shadow-color: $black !default;
$base-overlay-background: $black !default;
$base-border-color: $white !default;
$simple-background-color: $white !default;
$valid-value-color: $success-green !default;
$error-value-color: $error-red !default;
// Tell UI to use selected colors
$ui-base-color: $classic-base-color !default; // Darkest
$ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default;
// Variables for texts
$primary-text-color: $white !default;
$darker-text-color: $ui-primary-color !default;
$dark-text-color: $ui-base-lighter-color !default;
$secondary-text-color: $ui-secondary-color !default;
$highlight-text-color: $ui-highlight-color !default;
$action-button-color: $ui-base-lighter-color !default;
// For texts on inverted backgrounds
$inverted-text-color: $ui-base-color !default;
$lighter-text-color: $ui-base-lighter-color !default;
$light-text-color: $ui-primary-color !default;
// Language codes that uses CJK fonts
$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
// Variables for components
$media-modal-media-max-width: 100%;
// put margins on top and bottom of image to avoid the screen covered by image.
$media-modal-media-max-height: 80%;
$no-gap-breakpoint: 415px;
$font-sans-serif: 'mastodon-font-sans-serif' !default;
$font-display: 'mastodon-font-display' !default;
$font-monospace: 'mastodon-font-monospace' !default;

View File

@ -0,0 +1,568 @@
.hero-widget {
margin-bottom: 10px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
&__img {
width: 100%;
position: relative;
overflow: hidden;
border-radius: 4px 4px 0 0;
background: $base-shadow-color;
img {
object-fit: cover;
display: block;
width: 100%;
height: 100%;
margin: 0;
border-radius: 4px 4px 0 0;
}
}
&__text {
background: $ui-base-color;
padding: 20px;
border-radius: 0 0 4px 4px;
font-size: 15px;
color: $darker-text-color;
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
.emojione {
width: 20px;
height: 20px;
margin: -3px 0 0;
}
p {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
em {
display: inline;
margin: 0;
padding: 0;
font-weight: 700;
background: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: lighten($darker-text-color, 10%);
}
a {
color: $secondary-text-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
.endorsements-widget {
margin-bottom: 10px;
padding-bottom: 10px;
h4 {
padding: 10px;
text-transform: uppercase;
font-weight: 700;
font-size: 13px;
color: $darker-text-color;
}
.account {
padding: 10px 0;
&:last-child {
border-bottom: 0;
}
.account__display-name {
display: flex;
align-items: center;
}
.account__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
}
}
}
.box-widget {
padding: 20px;
border-radius: 4px;
background: $ui-base-color;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
}
.contact-widget,
.landing-page__information.contact-widget {
box-sizing: border-box;
padding: 20px;
min-height: 100%;
border-radius: 4px;
background: $ui-base-color;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
}
.contact-widget {
font-size: 15px;
color: $darker-text-color;
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
strong {
font-weight: 500;
}
p {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
&__mail {
margin-top: 10px;
a {
color: $primary-text-color;
text-decoration: none;
}
}
}
.moved-account-widget {
padding: 15px;
padding-bottom: 20px;
border-radius: 4px;
background: $ui-base-color;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
color: $secondary-text-color;
font-weight: 400;
margin-bottom: 10px;
strong,
a {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
a {
color: inherit;
text-decoration: underline;
&.mention {
text-decoration: none;
span {
text-decoration: none;
}
&:focus,
&:hover,
&:active {
text-decoration: none;
span {
text-decoration: underline;
}
}
}
}
&__message {
margin-bottom: 15px;
.fa {
margin-right: 5px;
color: $darker-text-color;
}
}
&__card {
.detailed-status__display-avatar {
position: relative;
cursor: pointer;
}
.detailed-status__display-name {
margin-bottom: 0;
text-decoration: none;
span {
font-weight: 400;
}
}
}
}
.memoriam-widget {
padding: 20px;
border-radius: 4px;
background: $base-shadow-color;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
font-size: 14px;
color: $darker-text-color;
margin-bottom: 10px;
}
.page-header {
background: lighten($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
padding: 60px 15px;
text-align: center;
margin: 10px 0;
h1 {
color: $primary-text-color;
font-size: 36px;
line-height: 1.1;
font-weight: 700;
margin-bottom: 10px;
}
p {
font-size: 15px;
color: $darker-text-color;
}
@media screen and (max-width: $no-gap-breakpoint) {
margin-top: 0;
background: lighten($ui-base-color, 4%);
h1 {
font-size: 24px;
}
}
}
.directory {
background: $ui-base-color;
border-radius: 4px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
&__tag {
box-sizing: border-box;
margin-bottom: 10px;
& > a,
& > div {
display: flex;
align-items: center;
justify-content: space-between;
background: $ui-base-color;
border-radius: 4px;
padding: 15px;
text-decoration: none;
color: inherit;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
}
& > a {
&:hover,
&:active,
&:focus {
background: lighten($ui-base-color, 8%);
}
}
&.active > a {
background: $ui-highlight-color;
cursor: default;
}
&.disabled > div {
opacity: 0.5;
cursor: default;
}
h4 {
flex: 1 1 auto;
font-size: 18px;
font-weight: 700;
color: $primary-text-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.fa {
color: $darker-text-color;
}
small {
display: block;
font-weight: 400;
font-size: 15px;
margin-top: 8px;
color: $darker-text-color;
}
}
&.active h4 {
&,
.fa,
small,
.trends__item__current {
color: $primary-text-color;
}
}
.avatar-stack {
flex: 0 0 auto;
width: (36px + 4px) * 3;
}
&.active .avatar-stack .account__avatar {
border-color: $ui-highlight-color;
}
.trends__item__current {
padding-right: 0;
}
}
}
.avatar-stack {
display: flex;
justify-content: flex-end;
.account__avatar {
flex: 0 0 auto;
width: 36px;
height: 36px;
border-radius: 50%;
position: relative;
margin-left: -10px;
background: darken($ui-base-color, 8%);
border: 2px solid $ui-base-color;
&:nth-child(1) {
z-index: 1;
}
&:nth-child(2) {
z-index: 2;
}
&:nth-child(3) {
z-index: 3;
}
}
}
.accounts-table {
width: 100%;
.account {
padding: 0;
border: 0;
}
strong {
font-weight: 700;
}
thead th {
text-align: center;
text-transform: uppercase;
color: $darker-text-color;
font-weight: 700;
padding: 10px;
&:first-child {
text-align: left;
}
}
tbody td {
padding: 15px 0;
vertical-align: middle;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
tbody tr:last-child td {
border-bottom: 0;
}
&__count {
width: 120px;
text-align: center;
font-size: 15px;
font-weight: 500;
color: $primary-text-color;
small {
display: block;
color: $darker-text-color;
font-weight: 400;
font-size: 14px;
}
}
&__comment {
width: 50%;
vertical-align: initial !important;
}
@media screen and (max-width: $no-gap-breakpoint) {
tbody td.optional {
display: none;
}
}
}
.moved-account-widget,
.memoriam-widget,
.box-widget,
.contact-widget,
.landing-page__information.contact-widget,
.directory,
.page-header {
@media screen and (max-width: $no-gap-breakpoint) {
margin-bottom: 0;
box-shadow: none;
border-radius: 0;
}
}
$maximum-width: 1235px;
$fluid-breakpoint: $maximum-width + 20px;
.statuses-grid {
min-height: 600px;
@media screen and (max-width: 640px) {
width: 100% !important; // Masonry layout is unnecessary at this width
}
&__item {
width: (960px - 20px) / 3;
@media screen and (max-width: $fluid-breakpoint) {
width: (940px - 20px) / 3;
}
@media screen and (max-width: 640px) {
width: 100%;
}
@media screen and (max-width: $no-gap-breakpoint) {
width: 100vw;
}
}
.detailed-status {
border-radius: 4px;
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 1px solid lighten($ui-base-color, 16%);
}
&.compact {
.detailed-status__meta {
margin-top: 15px;
}
.status__content {
font-size: 15px;
line-height: 20px;
.emojione {
width: 20px;
height: 20px;
margin: -3px 0 0;
}
.status__content__spoiler-link {
line-height: 20px;
margin: 0;
}
}
.media-gallery,
.status-card,
.video-player {
margin-top: 15px;
}
}
}
}
.notice-widget {
margin-bottom: 10px;
color: $darker-text-color;
p {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
a {
font-size: 14px;
line-height: 20px;
text-decoration: none;
font-weight: 500;
color: $ui-highlight-color;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}
// christmas snow
.round-button {
cursor: pointer;
margin: 0.5em;
width: 2em;
height: 2em;
display: inline-block;
}
.snow-button {
-webkit-border-radius: 100%;
-moz-border-radius: 100%;
border-radius: 100%;
background: $ui-secondary-color;
@extend .round-button;
.icon {
margin: 0.5em;
}
&:hover {
background: $ui-primary-color;
}
&.active {
background: $ui-highlight-color;
}
}

View File

@ -0,0 +1,56 @@
@import 'large_center';
// custom sheet made on https://www.cipherbliss.com
// Commonly used web colors
$black: #fff; // Black
$white: #000; // White
$success-green: #6bbd77 !default; // Padua
$error-red: #d4839b !default; // Cerise
$warning-red: #528dc8 !default; // Sunset Orange
$gold-star: #00ec84 !default; // Dark Goldenrod
// User Interface Colors
$ui-base-color: #313644; // Midnight Express
$ui-base-lighter-color: #bdd2d6;
$ui-primary-color: #a1ccff; // Echo Blue
$ui-secondary-color: #7fc0ff; // Pattens Blue
$ui-highlight-color: #00a7d1; // Summer Sky
// Variables for components
$media-modal-media-max-width: 100%;
// put margins on top and bottom of image to avoid the screen covered by image.
$media-modal-media-max-height: 80%;
// fix
.media-gallery {
margin-left: 0;
}
// then we import the rest of the world
@import 'application';
@import 'bliss/messaging';
.debug, .well {
padding: 0.5rem;
border: solid 1px greenyellow;
background: yellow;
color: #222;
}
.spacer {
display: block;
padding: 1em;
}
.small-texts {
.timelines {
.column-link {
}
span {
display: none;
}
}
}

View File

@ -0,0 +1,31 @@
.column {
width: 35em;
}
.drawer__inner__mastodon > img {
padding: 1rem;
box-sizing: border-box;
}
.compose-form__publish-button-wrapper .button {
background: #a1bded
}
div[aria-label="Accueil"],
div[aria-label="Notifications"] {
width: 27em;
}
.media-gallery {
width: 115%;
max-height: 500px;
}
.media-gallery,
.status-card.compact.interactive {
margin-left: -4.4em;
}
.status-card__image-image {
width: 100% !important;
}

View File

@ -5,7 +5,7 @@ $white: #ffffff;
$classic-base-color: #282c37; $classic-base-color: #282c37;
$classic-primary-color: #9baec8; $classic-primary-color: #9baec8;
$classic-secondary-color: #d9e1e8; $classic-secondary-color: #d9e1e8;
$classic-highlight-color: #2b90d9; $classic-highlight-color: #6274d9;
// Differences // Differences
$success-green: lighten(#3c754d, 8%); $success-green: lighten(#3c754d, 8%);
@ -21,7 +21,7 @@ $ui-highlight-color: #2b90d9;
$primary-text-color: $black !default; $primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default; $darker-text-color: $classic-base-color !default;
$dark-text-color: #444b5d; $dark-text-color: #45505d;
$action-button-color: #606984; $action-button-color: #606984;
$inverted-text-color: $black !default; $inverted-text-color: $black !default;

View File

@ -41,6 +41,28 @@ $inverted-text-color: $ui-base-color !default;
$lighter-text-color: $ui-base-lighter-color !default; $lighter-text-color: $ui-base-lighter-color !default;
$light-text-color: $ui-primary-color !default; $light-text-color: $ui-primary-color !default;
// custom sheet made on https://www.cipherbliss.com
// Commonly used web colors
$black: #fff; // Black
$white: #000; // White
$success-green: #6bbd77 !default; // Padua
$error-red: #d4839b !default; // Cerise
$warning-red: #528dc8 !default; // Sunset Orange
$gold-star: #00ec84 !default; // Dark Goldenrod
// User Interface Colors
$ui-base-color: #313644; // Midnight Express
$ui-base-lighter-color: #bdd2d6;
$ui-primary-color: #a1ccff; // Echo Blue
$ui-secondary-color: #7fc0ff; // Pattens Blue
$ui-highlight-color: #00a7d1; // Summer Sky
// Variables for components
$media-modal-media-max-width: 100%;
// put margins on top and bottom of image to avoid the screen covered by image.
$media-modal-media-max-height: 80%;
// fix
// Language codes that uses CJK fonts // Language codes that uses CJK fonts
$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW; $cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;

16
app/models/user_group.rb Normal file
View File

@ -0,0 +1,16 @@
# == Schema Information
#
# Table name: user_groups
#
# id :bigint(8) not null, primary key
# name :string not null
# createdAt :datetime
# visibility :string
# account_id :integer
# creator_id :integer
# created_at :datetime not null
# updated_at :datetime not null
#
class UserGroup < ApplicationRecord
end

View File

@ -0,0 +1,3 @@
class UserGroupSerializer < ActiveModel::Serializer
attributes :id, :name, :createAt, :visibility, :members
end

View File

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

View File

@ -20,3 +20,11 @@
- else - else
= table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}") = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}")
= table_link_to 'globe', t('admin.accounts.public'), ActivityPub::TagManager.instance.url_for(account) = table_link_to 'globe', t('admin.accounts.public'), ActivityPub::TagManager.instance.url_for(account)
%td
= number_with_delimiter account.statuses_count
%td
= number_with_delimiter account.following_count
%td
= number_with_delimiter account.followers_count
%td
= table_link_to 'trash', t('admin.accounts.web'), web_path("accounts/#{account.id}/action/new?type=suspend")

View File

@ -43,7 +43,11 @@
%th= t('admin.accounts.role') %th= t('admin.accounts.role')
%th= t('admin.accounts.most_recent_ip') %th= t('admin.accounts.most_recent_ip')
%th= t('admin.accounts.most_recent_activity') %th= t('admin.accounts.most_recent_activity')
%th %th links
%th statuses
%th following
%th followers
%th nuke
%tbody %tbody
= render @accounts = render @accounts

View File

@ -21,7 +21,7 @@
%hr.spacer/ %hr.spacer/
.fields-group .fields-group
= f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked') = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked'), recommended: true
.fields-group .fields-group
= f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot') = f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot')

View File

@ -0,0 +1,16 @@
.container.group-form
%p
WORK IN PROGRESS
= simple_form_for(@user_group) do |f|
= f.error_notification
.form-inputs
= f.input :name
= f.input :createdAt
= f.input :visibility
= f.input :account_id
.form-actions
= f.button :submit

View File

@ -0,0 +1,10 @@
- content_for :page_title do
= 'Modifier le groupe'
%h1 Editing user_group
= render 'form'
= link_to 'Show', @user_group
\|
= link_to 'Back', user_groups_path

View File

@ -0,0 +1,34 @@
- content_for :page_title do
= 'Groupes'
.container
.page-header
%h1 Listing user_groups
%table.table-responsive.table-striped
%thead
%tr
%th Name
%th Createat
%th Visibility
%th Creator ID
%th Statuses count
%th
%th
%th
%tbody
- @user_groups.each do |user_group|
%tr
%td= user_group.name
%td= user_group.created_at
%td= user_group.visibility
%td= user_group.account_id
%td= "0"
%td= link_to 'Show', user_group
%td= link_to 'Edit', edit_user_group_path(user_group)
%td= link_to 'Destroy', user_group, :method => :delete, :data => { :confirm => 'Are you sure?' }
%br
.text-btn
= link_to 'New User group', new_user_group_path

View File

@ -0,0 +1,8 @@
- content_for :page_title do
= 'Nouveau groupe'
.container
%h1 New user_group
= render 'form'
= link_to 'Back', user_groups_path

View File

@ -0,0 +1,18 @@
%p#notice= notice
%p
%b Name:
= @user_group.name
%p
%b Createat:
= @user_group.created_at
%p
%b Visibility:
= @user_group.visibility
%p
%b Creator:
= @user_group.account_id
= link_to 'Edit', edit_user_group_path(@user_group)
\|
= link_to 'Back', user_groups_path

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Mastodon
module Version
module_function
def source_base_url
'https://framagit.org/tykayn/mastodon'
end
end
end

View File

@ -1137,7 +1137,7 @@ fr:
title: "%{instance} Conditions dutilisation et politique de confidentialité" title: "%{instance} Conditions dutilisation et politique de confidentialité"
themes: themes:
contrast: Mastodon (Contraste élevé) contrast: Mastodon (Contraste élevé)
default: Mastodon (Sombre) default: Bliss (Sombre)
mastodon-light: Mastodon (Clair) mastodon-light: Mastodon (Clair)
time: time:
formats: formats:

View File

@ -2,7 +2,7 @@
# important settings can be changed from the admin interface. # important settings can be changed from the admin interface.
defaults: &defaults defaults: &defaults
site_title: Mastodon site_title: Mastodon Bliss
site_short_description: '' site_short_description: ''
site_description: '' site_description: ''
site_extended_description: '' site_extended_description: ''
@ -53,6 +53,7 @@ defaults: &defaults
must_be_following_dm: false must_be_following_dm: false
reserved_usernames: reserved_usernames:
- admin - admin
- tykayn
- support - support
- help - help
- root - root

View File

@ -1,3 +1,5 @@
default: styles/application.scss default: styles/bliss.scss
custom: styles/custom.scss
mastodon: styles/custom.scss
contrast: styles/contrast.scss contrast: styles/contrast.scss
mastodon-light: styles/mastodon-light.scss mastodon-light: styles/mastodon-light.scss

View File

@ -25,7 +25,7 @@ function formatPublicPath(host = '', path = '') {
} }
const output = { const output = {
path: resolve('public', settings.public_output_path), path : resolve('public', settings.public_output_path),
publicPath: formatPublicPath(env.CDN_HOST, settings.public_output_path), publicPath: formatPublicPath(env.CDN_HOST, settings.public_output_path),
}; };

View File

View File

@ -48,5 +48,3 @@ setLocale({messages, localeData});
}); });
module.exports = outPaths; module.exports = outPaths;

View File

@ -26,16 +26,16 @@ if (process.env.S3_ENABLED === 'true') {
} }
module.exports = merge(sharedConfig, { module.exports = merge(sharedConfig, {
mode: 'production', mode : 'production',
devtool: 'source-map', devtool : 'source-map',
stats: 'normal', stats : 'normal',
bail: true, bail : true,
optimization: { optimization: {
minimize: true, minimize : true,
minimizer: [ minimizer: [
new TerserPlugin({ new TerserPlugin({
cache: true, cache : true,
parallel: true, parallel : true,
sourceMap: true, sourceMap: true,
}), }),
], ],
@ -44,21 +44,21 @@ module.exports = merge(sharedConfig, {
plugins: [ plugins: [
new CompressionPlugin({ new CompressionPlugin({
filename: '[path].gz[query]', filename: '[path].gz[query]',
cache: true, cache : true,
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/, test : /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
}), }),
new BundleAnalyzerPlugin({ // generates report.html new BundleAnalyzerPlugin({ // generates report.html
analyzerMode: 'static', analyzerMode: 'static',
openAnalyzer: false, openAnalyzer: false,
logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout logLevel : 'silent', // do not bother Webpacker, who runs with --json and parses stdout
}), }),
new OfflinePlugin({ new OfflinePlugin({
publicPath: output.publicPath, // sw.js must be served from the root to avoid scope issues publicPath : output.publicPath, // sw.js must be served from the root to avoid scope issues
safeToUseOptionalCaches: true, safeToUseOptionalCaches: true,
caches: { caches : {
main: [':rest:'], main : [':rest:'],
additional: [':externals:'], additional: [':externals:'],
optional: [ optional : [
'**/locale_*.js', // don't fetch every locale; the user only needs one '**/locale_*.js', // don't fetch every locale; the user only needs one
'**/*_polyfills-*.js', // the user may not need polyfills '**/*_polyfills-*.js', // the user may not need polyfills
'**/*.woff2', // the user may have system-fonts enabled '**/*.woff2', // the user may have system-fonts enabled
@ -71,11 +71,11 @@ module.exports = merge(sharedConfig, {
'**/*.ogg', '**/*.ogg',
], ],
}, },
externals: [ externals : [
'/emoji/1f602.svg', // used for emoji picker dropdown '/emoji/1f602.svg', // used for emoji picker dropdown
'/emoji/sheet_10.png', // used in emoji-mart '/emoji/sheet_10.png', // used in emoji-mart
], ],
excludes: [ excludes : [
'**/*.gz', '**/*.gz',
'**/*.map', '**/*.map',
'stats.json', 'stats.json',
@ -86,12 +86,12 @@ module.exports = merge(sharedConfig, {
'**/*-webfont-*.svg', '**/*-webfont-*.svg',
'**/*.woff', '**/*.woff',
], ],
ServiceWorker: { ServiceWorker : {
entry: `imports-loader?ATTACHMENT_HOST=>${encodeURIComponent(JSON.stringify(attachmentHost))}!${encodeURI(path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'))}`, entry : `imports-loader?ATTACHMENT_HOST=>${encodeURIComponent(JSON.stringify(attachmentHost))}!${encodeURI(path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'))}`,
cacheName: 'mastodon', cacheName : 'mastodon',
output: '../assets/sw.js', output : '../assets/sw.js',
publicPath: '/sw.js', publicPath: '/sw.js',
minify: true, minify : true,
}, },
}), }),
], ],

View File

@ -31,31 +31,31 @@ module.exports = {
Object.keys(themes).reduce((themePaths, name) => { Object.keys(themes).reduce((themePaths, name) => {
themePaths[name] = resolve(join(settings.source_path, themes[name])); themePaths[name] = resolve(join(settings.source_path, themes[name]));
return themePaths; return themePaths;
}, {}) }, {}),
), ),
output: { output: {
filename: 'js/[name]-[chunkhash].js', filename : 'js/[name]-[chunkhash].js',
chunkFilename: 'js/[name]-[chunkhash].chunk.js', chunkFilename : 'js/[name]-[chunkhash].chunk.js',
hotUpdateChunkFilename: 'js/[id]-[hash].hot-update.js', hotUpdateChunkFilename: 'js/[id]-[hash].hot-update.js',
path: output.path, path : output.path,
publicPath: output.publicPath, publicPath : output.publicPath,
}, },
optimization: { optimization: {
runtimeChunk: { runtimeChunk : {
name: 'common', name: 'common',
}, },
splitChunks: { splitChunks : {
cacheGroups: { cacheGroups: {
default: false, default: false,
vendors: false, vendors: false,
common: { common : {
name: 'common', name : 'common',
chunks: 'all', chunks : 'all',
minChunks: 2, minChunks: 2,
minSize: 0, minSize : 0,
test: /^(?!.*[\\\/]node_modules[\\\/]react-intl[\\\/]).+$/, test : /^(?!.*[\\\/]node_modules[\\\/]react-intl[\\\/]).+$/,
}, },
}, },
}, },
@ -73,17 +73,17 @@ module.exports = {
// temporary fix for https://github.com/ReactTraining/react-router/issues/5576 // temporary fix for https://github.com/ReactTraining/react-router/issues/5576
// to reduce bundle size // to reduce bundle size
resource.request = resource.request.replace(/^history/, 'history/es'); resource.request = resource.request.replace(/^history/, 'history/es');
} },
), ),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: 'css/[name]-[contenthash:8].css', filename : 'css/[name]-[contenthash:8].css',
chunkFilename: 'css/[name]-[contenthash:8].chunk.css', chunkFilename: 'css/[name]-[contenthash:8].chunk.css',
}), }),
new AssetsManifestPlugin({ new AssetsManifestPlugin({
integrity: false, integrity : false,
entrypoints: true, entrypoints: true,
writeToDisk: true, writeToDisk: true,
publicPath: true, publicPath : true,
}), }),
new CopyPlugin([ new CopyPlugin([
{ from: 'node_modules/tesseract.js/dist/worker.min.js', to: 'ocr' }, { from: 'node_modules/tesseract.js/dist/worker.min.js', to: 'ocr' },
@ -93,7 +93,7 @@ module.exports = {
resolve: { resolve: {
extensions: settings.extensions, extensions: settings.extensions,
modules: [ modules : [
resolve(settings.source_path), resolve(settings.source_path),
'node_modules', 'node_modules',
], ],

View File

@ -41,7 +41,6 @@ const validateLanguages = (languages, validators) => {
console.error(` console.error(`
Error: Specified invalid LANGUAGES: Error: Specified invalid LANGUAGES:
${invalidLanguages.map(({ language, error }) => `* ${language}: ${error.message}`).join('\n')} ${invalidLanguages.map(({ language, error }) => `* ${language}: ${error.message}`).join('\n')}
Use yarn "manage:translations -- --help" for usage information Use yarn "manage:translations -- --help" for usage information
`); `);
process.exit(1); process.exit(1);
@ -49,12 +48,9 @@ Use yarn "manage:translations -- --help" for usage information
}; };
const usage = `Usage: yarn manage:translations [OPTIONS] [LANGUAGES] const usage = `Usage: yarn manage:translations [OPTIONS] [LANGUAGES]
Manage JavaScript translation files in Mastodon. Generates and update translations in translationsDirectory: ${translationsDirectory} Manage JavaScript translation files in Mastodon. Generates and update translations in translationsDirectory: ${translationsDirectory}
LANGUAGES LANGUAGES
The RFC5646 language tag for the language you want to test or fix. If you want to input multiple languages, separate them with space. The RFC5646 language tag for the language you want to test or fix. If you want to input multiple languages, separate them with space.
Available languages: Available languages:
${availableLanguages.join(', ')} ${availableLanguages.join(', ')}
`; `;
@ -62,10 +58,10 @@ ${availableLanguages.join(', ')}
const { argv } = require('yargs') const { argv } = require('yargs')
.usage(usage) .usage(usage)
.option('f', { .option('f', {
alias: 'force', alias : 'force',
default: false, default : false,
describe: 'force using the provided languages. create files if not exists.', describe: 'force using the provided languages. create files if not exists.',
type: 'boolean', type : 'boolean',
}); });
// check if message directory exists // check if message directory exists
@ -93,7 +89,7 @@ manageTranslations({
detectDuplicateIds: false, detectDuplicateIds: false,
singleMessagesFile: true, singleMessagesFile: true,
languages, languages,
jsonOptions: { jsonOptions : {
trailingNewline: true, trailingNewline: true,
}, },
}); });

View File

@ -1,4 +1,4 @@
class AddCommentsToDomainBlocks < ActiveRecord::Migration[5.2] class AddCommentsToDomainBlocks < ActiveRecordinteger::Migration[5.2]
def change def change
add_column :domain_blocks, :private_comment, :text add_column :domain_blocks, :private_comment, :text
add_column :domain_blocks, :public_comment, :text add_column :domain_blocks, :public_comment, :text

View File

@ -0,0 +1,14 @@
class CreateUserGroups < ActiveRecord::Migration[5.2]
def change
create_table :user_groups do |t|
t.string :name, null: false
t.datetime :createdAt
t.string :visibility
t.integer :account_id
t.integer :creator_id
t.integer :statuses_count
t.timestamps
end
end
end

View File

@ -200,11 +200,11 @@ ActiveRecord::Schema.define(version: 2019_12_12_003415) do
t.bigint "user_id" t.bigint "user_id"
t.string "dump_file_name" t.string "dump_file_name"
t.string "dump_content_type" t.string "dump_content_type"
t.bigint "dump_file_size"
t.datetime "dump_updated_at" t.datetime "dump_updated_at"
t.boolean "processed", default: false, null: false t.boolean "processed", default: false, null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.bigint "dump_file_size"
end end
create_table "blocks", force: :cascade do |t| create_table "blocks", force: :cascade do |t|
@ -703,6 +703,30 @@ ActiveRecord::Schema.define(version: 2019_12_12_003415) do
t.index ["tag_id", "status_id"], name: "index_statuses_tags_on_tag_id_and_status_id", unique: true t.index ["tag_id", "status_id"], name: "index_statuses_tags_on_tag_id_and_status_id", unique: true
end end
create_table "stream_entries", force: :cascade do |t|
t.bigint "activity_id"
t.string "activity_type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "hidden", default: false, null: false
t.bigint "account_id"
t.index ["account_id", "activity_type", "id"], name: "index_stream_entries_on_account_id_and_activity_type_and_id"
t.index ["activity_id", "activity_type"], name: "index_stream_entries_on_activity_id_and_activity_type"
end
create_table "subscriptions", force: :cascade do |t|
t.string "callback_url", default: "", null: false
t.string "secret"
t.datetime "expires_at"
t.boolean "confirmed", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "last_successful_delivery_at"
t.string "domain"
t.bigint "account_id", null: false
t.index ["account_id", "callback_url"], name: "index_subscriptions_on_account_id_and_callback_url", unique: true
end
create_table "tags", force: :cascade do |t| create_table "tags", force: :cascade do |t|
t.string "name", default: "", null: false t.string "name", default: "", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
@ -728,6 +752,16 @@ ActiveRecord::Schema.define(version: 2019_12_12_003415) do
t.index ["uri"], name: "index_tombstones_on_uri" t.index ["uri"], name: "index_tombstones_on_uri"
end end
create_table "user_groups", force: :cascade do |t|
t.string "name", null: false
t.datetime "createdAt"
t.string "visibility"
t.integer "account_id"
t.integer "creator_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "user_invite_requests", force: :cascade do |t| create_table "user_invite_requests", force: :cascade do |t|
t.bigint "user_id" t.bigint "user_id"
t.text "text" t.text "text"

15182
package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

View File

@ -67,8 +67,8 @@
"@babel/preset-env": "^7.7.6", "@babel/preset-env": "^7.7.6",
"@babel/preset-react": "^7.7.4", "@babel/preset-react": "^7.7.4",
"@babel/runtime": "^7.7.6", "@babel/runtime": "^7.7.6",
"@gamestdio/websocket": "^0.3.2",
"@clusterws/cws": "^0.16.0", "@clusterws/cws": "^0.16.0",
"@gamestdio/websocket": "^0.3.2",
"array-includes": "^3.1.0", "array-includes": "^3.1.0",
"arrow-key-navigation": "^1.1.0", "arrow-key-navigation": "^1.1.0",
"autoprefixer": "^9.7.3", "autoprefixer": "^9.7.3",
@ -118,8 +118,12 @@
"offline-plugin": "^5.0.7", "offline-plugin": "^5.0.7",
"path-complete-extname": "^1.0.0", "path-complete-extname": "^1.0.0",
"pg": "^6.4.0", "pg": "^6.4.0",
"postcss": "^7.0.24",
"postcss-flexbugs-fixes": "^4.1.0",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
"postcss-object-fit-images": "^1.1.2", "postcss-object-fit-images": "^1.1.2",
"postcss-preset-env": "^6.7.0",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"punycode": "^2.1.0", "punycode": "^2.1.0",
"rails-ujs": "^5.2.4", "rails-ujs": "^5.2.4",

View File

@ -1,7 +1,12 @@
module.exports = ({ env }) => ({ module.exports = {
plugins: { plugins: [
autoprefixer: {}, require('postcss-import'),
'postcss-object-fit-images': {}, require('postcss-flexbugs-fixes'),
cssnano: env === 'production' ? {} : false, require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009'
}, },
}); stage: 3
})
]
}

17
precompile-log.txt Normal file
View File

@ -0,0 +1,17 @@
yarn install v1.16.0
warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
[1/6] Validating package.json...
[2/6] Resolving packages...
success Already up-to-date.
Done in 0.51s.
yarn install v1.16.0
warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
[1/6] Validating package.json...
[2/6] Resolving packages...
success Already up-to-date.
Done in 0.49s.
Everything's up-to-date. Nothing to do
Rendering errors/500.html.haml within layouts/error
Rendered errors/500.html.haml within layouts/error (604.9ms)
[Webpacker] Everything's up-to-date. Nothing to do
[Webpacker] Everything's up-to-date. Nothing to do

View File

@ -0,0 +1,141 @@
require 'rails_helper'
# This spec was generated by rspec-rails when you ran the scaffold generator.
# It demonstrates how one might use RSpec to specify the controller code that
# was generated by Rails when you ran the scaffold generator.
#
# It assumes that the implementation code is generated by the rails scaffold
# generator. If you are using any extension libraries to generate different
# controller code, this generated spec may or may not pass.
#
# It only uses APIs available in rails and/or rspec-rails. There are a number
# of tools you can use to make these specs even more expressive, but we're
# sticking to rails and rspec-rails APIs to keep things simple and stable.
#
# Compared to earlier versions of this generator, there is very limited use of
# stubs and message expectations in this spec. Stubs are only used when there
# is no simpler way to get a handle on the object needed for the example.
# Message expectations are only used when there is no simpler way to specify
# that an instance is receiving a specific message.
#
# Also compared to earlier versions of this generator, there are no longer any
# expectations of assigns and templates rendered. These features have been
# removed from Rails core in Rails 5, but can be added back in via the
# `rails-controller-testing` gem.
RSpec.describe UserGroupsController, type: :controller do
# This should return the minimal set of attributes required to create a valid
# UserGroup. As you add validations to UserGroup, be sure to
# adjust the attributes here as well.
let(:valid_attributes) {
skip("Add a hash of attributes valid for your model")
}
let(:invalid_attributes) {
skip("Add a hash of attributes invalid for your model")
}
# This should return the minimal set of values that should be in the session
# in order to pass any filters (e.g. authentication) defined in
# UserGroupsController. Be sure to keep this updated too.
let(:valid_session) { {} }
describe "GET #index" do
it "returns a success response" do
UserGroup.create! valid_attributes
get :index, params: {}, session: valid_session
expect(response).to be_successful
end
end
describe "GET #show" do
it "returns a success response" do
user_group = UserGroup.create! valid_attributes
get :show, params: {id: user_group.to_param}, session: valid_session
expect(response).to be_successful
end
end
describe "GET #new" do
it "returns a success response" do
get :new, params: {}, session: valid_session
expect(response).to be_successful
end
end
describe "GET #edit" do
it "returns a success response" do
user_group = UserGroup.create! valid_attributes
get :edit, params: {id: user_group.to_param}, session: valid_session
expect(response).to be_successful
end
end
describe "POST #create" do
context "with valid params" do
it "creates a new UserGroup" do
expect {
post :create, params: {user_group: valid_attributes}, session: valid_session
}.to change(UserGroup, :count).by(1)
end
it "redirects to the created user_group" do
post :create, params: {user_group: valid_attributes}, session: valid_session
expect(response).to redirect_to(UserGroup.last)
end
end
context "with invalid params" do
it "returns a success response (i.e. to display the 'new' template)" do
post :create, params: {user_group: invalid_attributes}, session: valid_session
expect(response).to be_successful
end
end
end
describe "PUT #update" do
context "with valid params" do
let(:new_attributes) {
skip("Add a hash of attributes valid for your model")
}
it "updates the requested user_group" do
user_group = UserGroup.create! valid_attributes
put :update, params: {id: user_group.to_param, user_group: new_attributes}, session: valid_session
user_group.reload
skip("Add assertions for updated state")
end
it "redirects to the user_group" do
user_group = UserGroup.create! valid_attributes
put :update, params: {id: user_group.to_param, user_group: valid_attributes}, session: valid_session
expect(response).to redirect_to(user_group)
end
end
context "with invalid params" do
it "returns a success response (i.e. to display the 'edit' template)" do
user_group = UserGroup.create! valid_attributes
put :update, params: {id: user_group.to_param, user_group: invalid_attributes}, session: valid_session
expect(response).to be_successful
end
end
end
describe "DELETE #destroy" do
it "destroys the requested user_group" do
user_group = UserGroup.create! valid_attributes
expect {
delete :destroy, params: {id: user_group.to_param}, session: valid_session
}.to change(UserGroup, :count).by(-1)
end
it "redirects to the user_groups list" do
user_group = UserGroup.create! valid_attributes
delete :destroy, params: {id: user_group.to_param}, session: valid_session
expect(response).to redirect_to(user_groups_url)
end
end
end

View File

@ -0,0 +1,6 @@
Fabricator(:user_group) do
name "MyString"
createAt "2019-08-27 14:04:56"
visibility "MyString"
members ""
end

View File

@ -0,0 +1,15 @@
require 'rails_helper'
# Specs in this file have access to a helper object that includes
# the UserGroupsHelper. For example:
#
# describe UserGroupsHelper do
# describe "string concat" do
# it "concats two strings with spaces" do
# expect(helper.concat_strings("this","that")).to eq("this that")
# end
# end
# end
RSpec.describe UserGroupsHelper, type: :helper do
pending "add some examples to (or delete) #{__FILE__}"
end

View File

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe UserGroup, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View File

@ -0,0 +1,10 @@
require 'rails_helper'
RSpec.describe "UserGroups", type: :request do
describe "GET /user_groups" do
it "works! (now write some real specs)" do
get user_groups_path
expect(response).to have_http_status(200)
end
end
end

View File

@ -0,0 +1,38 @@
require "rails_helper"
RSpec.describe UserGroupsController, type: :routing do
describe "routing" do
it "routes to #index" do
expect(:get => "/user_groups").to route_to("user_groups#index")
end
it "routes to #new" do
expect(:get => "/user_groups/new").to route_to("user_groups#new")
end
it "routes to #show" do
expect(:get => "/user_groups/1").to route_to("user_groups#show", :id => "1")
end
it "routes to #edit" do
expect(:get => "/user_groups/1/edit").to route_to("user_groups#edit", :id => "1")
end
it "routes to #create" do
expect(:post => "/user_groups").to route_to("user_groups#create")
end
it "routes to #update via PUT" do
expect(:put => "/user_groups/1").to route_to("user_groups#update", :id => "1")
end
it "routes to #update via PATCH" do
expect(:patch => "/user_groups/1").to route_to("user_groups#update", :id => "1")
end
it "routes to #destroy" do
expect(:delete => "/user_groups/1").to route_to("user_groups#destroy", :id => "1")
end
end
end

Some files were not shown because too many files have changed in this diff Show More