Merge branch 'messaging'

messaging
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
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,23 +1,19 @@
import api, { getLinks } from '../api';
import {
importFetchedAccounts,
importFetchedStatuses,
importFetchedStatus,
} from './importer';
import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer';
export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT';
export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT';
export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT';
export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST';
export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS';
export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL';
export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL';
export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
export const CONVERSATIONS_DELETE_REQUEST = 'CONVERSATIONS_DELETE_REQUEST';
export const CONVERSATIONS_DELETE_SUCCESS = 'CONVERSATIONS_DELETE_SUCCESS';
export const CONVERSATIONS_DELETE_FAIL = 'CONVERSATIONS_DELETE_FAIL';
export const CONVERSATIONS_DELETE_FAIL = 'CONVERSATIONS_DELETE_FAIL';
export const mountConversations = () => ({
type: CONVERSATIONS_MOUNT,
@ -30,7 +26,7 @@ export const unmountConversations = () => ({
export const markConversationRead = conversationId => (dispatch, getState) => {
dispatch({
type: CONVERSATIONS_READ,
id: conversationId,
id : conversationId,
});
api(getState).post(`/api/v1/conversations/${conversationId}/read`);

View File

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

View File

@ -12,7 +12,7 @@ import classNames from 'classnames';
const textAtCursorMatchesToken = (str, caretPosition) => {
let word;
let left = str.slice(0, caretPosition).search(/\S+$/);
let left = str.slice(0, caretPosition).search(/\S+$/);
let right = str.slice(caretPosition).search(/\s/);
if (right < 0) {
@ -37,34 +37,37 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
export default class AutosuggestTextarea extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
suggestions: ImmutablePropTypes.list,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
onSuggestionSelected: PropTypes.func.isRequired,
value : PropTypes.string,
suggestions : ImmutablePropTypes.list,
disabled : PropTypes.bool,
placeholder : PropTypes.string,
onSuggestionSelected : PropTypes.func.isRequired,
onSuggestionsClearRequested: PropTypes.func.isRequired,
onSuggestionsFetchRequested: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
onPaste: PropTypes.func.isRequired,
autoFocus: PropTypes.bool,
onChange : PropTypes.func.isRequired,
onKeyUp : PropTypes.func,
onKeyDown : PropTypes.func,
onPaste : PropTypes.func.isRequired,
autoFocus : PropTypes.bool,
directMessage : PropTypes.bool,
directMessageRecipient : PropTypes.string,
};
static defaultProps = {
autoFocus: true,
autoFocus : true,
directMessage: false,
};
state = {
suggestionsHidden: true,
focused: false,
suggestionsHidden : true,
focused : false,
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0,
lastToken : null,
tokenStart : 0,
};
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) {
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
@ -75,7 +78,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
}
this.props.onChange(e);
}
};
onKeyDown = (e) => {
const { suggestions, disabled } = this.props;
@ -92,7 +95,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
return;
}
switch(e.key) {
switch (e.key) {
case 'Escape':
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector('.ui').parentElement.focus();
@ -124,7 +127,6 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
}
break;
}
@ -133,27 +135,27 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
}
this.props.onKeyDown(e);
}
};
onBlur = () => {
this.setState({ suggestionsHidden: true, focused: false });
}
};
onFocus = (e) => {
this.setState({ focused: true });
if (this.props.onFocus) {
this.props.onFocus(e);
}
}
};
onSuggestionClick = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textarea.focus();
}
};
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false });
}
@ -161,14 +163,14 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
setTextarea = (c) => {
this.textarea = c;
}
};
onPaste = (e) => {
if (e.clipboardData && e.clipboardData.files.length === 1) {
this.props.onPaste(e.clipboardData.files);
e.preventDefault();
}
}
};
renderSuggestion = (suggestion, i) => {
const { selectedSuggestion } = this.state;
@ -176,23 +178,30 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
if (suggestion.type === 'emoji') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
key = suggestion.id;
} else if (suggestion.type === 'hashtag') {
inner = <AutosuggestHashtag tag={suggestion} />;
key = suggestion.name;
key = suggestion.name;
} else if (suggestion.type === 'account') {
inner = <AutosuggestAccountContainer id={suggestion.id} />;
key = suggestion.id;
key = suggestion.id;
}
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}
</div>
</div >
);
}
};
render () {
render() {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' };
@ -202,10 +211,13 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
}
return [
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
<div
className='compose-form__autosuggest-wrapper'
key='autosuggest-wrapper'
>
<div className='autosuggest-textarea'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<label >
<span style={{ display: 'none' }}>{placeholder}</span >
<Textarea
inputRef={this.setTextarea}
@ -223,16 +235,21 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
style={style}
aria-autocomplete='list'
/>
</label>
</div>
</label >
</div >
{children}
</div>,
</div >,
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
<div
className='autosuggest-textarea__suggestions-wrapper'
key='suggestions-wrapper'
>
<div
className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}
>
{suggestions.map(this.renderSuggestion)}
</div>
</div>,
</div >
</div >,
];
}

View File

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

View File

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

View File

@ -6,41 +6,41 @@ import Icon from 'mastodon/components/icon';
export default class IconButton extends React.PureComponent {
static propTypes = {
className: PropTypes.string,
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
onClick: PropTypes.func,
className : PropTypes.string,
title : PropTypes.string.isRequired,
icon : PropTypes.string.isRequired,
onClick : PropTypes.func,
onMouseDown: PropTypes.func,
onKeyDown: PropTypes.func,
onKeyPress: PropTypes.func,
size: PropTypes.number,
active: PropTypes.bool,
pressed: PropTypes.bool,
expanded: PropTypes.bool,
style: PropTypes.object,
onKeyDown : PropTypes.func,
onKeyPress : PropTypes.func,
size : PropTypes.number,
active : PropTypes.bool,
pressed : PropTypes.bool,
expanded : PropTypes.bool,
style : PropTypes.object,
activeStyle: PropTypes.object,
disabled: PropTypes.bool,
inverted: PropTypes.bool,
animate: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
disabled : PropTypes.bool,
inverted : PropTypes.bool,
animate : PropTypes.bool,
overlay : PropTypes.bool,
tabIndex : PropTypes.string,
};
static defaultProps = {
size: 18,
active: false,
size : 18,
active : false,
disabled: false,
animate: false,
overlay: false,
animate : false,
overlay : false,
tabIndex: '0',
};
state = {
activate: false,
activate : false,
deactivate: false,
}
};
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
if (!nextProps.animate) return;
if (this.props.active && !nextProps.active) {
@ -50,38 +50,38 @@ export default class IconButton extends React.PureComponent {
}
}
handleClick = (e) => {
handleClick = (e) => {
e.preventDefault();
if (!this.props.disabled) {
this.props.onClick(e);
}
}
};
handleKeyPress = (e) => {
if (this.props.onKeyPress && !this.props.disabled) {
this.props.onKeyPress(e);
}
}
};
handleMouseDown = (e) => {
if (!this.props.disabled && this.props.onMouseDown) {
this.props.onMouseDown(e);
}
}
};
handleKeyDown = (e) => {
if (!this.props.disabled && this.props.onKeyDown) {
this.props.onKeyDown(e);
}
}
};
render () {
render() {
const style = {
fontSize: `${this.props.size}px`,
width: `${this.props.size * 1.28571429}px`,
height: `${this.props.size * 1.28571429}px`,
lineHeight: `${this.props.size}px`,
// fontSize: `${this.props.size}px`,
// width: `${this.props.size * 1.28571429}px`,
// height: `${this.props.size * 1.28571429}px`,
// lineHeight: `${this.props.size}px`,
...this.props.style,
...(this.props.active ? this.props.activeStyle : {}),
};
@ -128,8 +128,12 @@ export default class IconButton extends React.PureComponent {
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
</button>
<Icon
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 AttachmentList from './attachment_list';
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 { 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 classNames from 'classnames';
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
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
import imageShowThread from '../../images/icon_reply.svg';
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']);
@ -59,33 +59,37 @@ class Status extends ImmutablePureComponent {
};
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
otherAccounts: ImmutablePropTypes.list,
onClick: PropTypes.func,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
onPin: PropTypes.func,
onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func,
onBlock: PropTypes.func,
onEmbed: PropTypes.func,
onHeightChange: PropTypes.func,
onToggleHidden: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
showThread: PropTypes.bool,
getScrollPosition: PropTypes.func,
status : ImmutablePropTypes.map,
account : ImmutablePropTypes.map,
otherAccounts : ImmutablePropTypes.list,
onClick : PropTypes.func,
onReply : PropTypes.func,
onFavourite : PropTypes.func,
onReblog : PropTypes.func,
onDelete : PropTypes.func,
onDirect : PropTypes.func,
onMention : PropTypes.func,
onPin : PropTypes.func,
onOpenMedia : PropTypes.func,
onOpenVideo : PropTypes.func,
onBlock : PropTypes.func,
onEmbed : PropTypes.func,
onHeightChange : PropTypes.func,
onToggleHidden : PropTypes.func,
muted : PropTypes.bool,
hidden : PropTypes.bool,
unread : PropTypes.bool,
onMoveUp : PropTypes.func,
onMoveDown : PropTypes.func,
showThread : PropTypes.bool,
threadsCompile : PropTypes.bool,
getScrollPosition : PropTypes.func,
updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
cacheMediaWidth : PropTypes.func,
cachedMediaWidth : PropTypes.number,
};
static defaultProps = {
threadsCompile: true,
};
// Avoid checking props that are functions (and whose equality will always
@ -99,15 +103,26 @@ class Status extends ImmutablePureComponent {
state = {
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
componentDidMount () {
componentDidMount() {
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
}
getSnapshotBeforeUpdate () {
getSnapshotBeforeUpdate() {
if (this.props.getScrollPosition) {
return this.props.getScrollPosition();
} else {
@ -115,20 +130,9 @@ 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
componentDidUpdate (prevProps, prevState, snapshot) {
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
componentDidUpdate(prevProps, prevState, snapshot) {
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
if (doShowCard && !this.didShowCard) {
this.didShowCard = true;
@ -154,7 +158,7 @@ class Status extends ImmutablePureComponent {
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
}
};
handleClick = () => {
if (this.props.onClick) {
@ -168,7 +172,7 @@ class Status extends ImmutablePureComponent {
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
}
};
handleExpandClick = (e) => {
if (this.props.onClick) {
@ -184,7 +188,7 @@ class Status extends ImmutablePureComponent {
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
}
}
};
handleAccountClick = (e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
@ -192,27 +196,36 @@ class Status extends ImmutablePureComponent {
e.preventDefault();
this.context.router.history.push(`/accounts/${id}`);
}
}
};
handleExpandedToggle = () => {
this.props.onToggleHidden(this._properStatus());
};
renderLoadingMediaGallery () {
return <div className='media-gallery' style={{ height: '110px' }} />;
renderLoadingMediaGallery() {
return (<div
className='media-gallery'
style={{ height: '110px' }}
/>);
}