This commit is contained in:
mgabdev 2020-02-28 10:20:47 -05:00
parent 0bd1eb2c77
commit 3ca4ffcc6b
77 changed files with 6110 additions and 1427 deletions

View File

@ -1,10 +0,0 @@
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
export function openDropdownMenu(id, placement, keyboard) {
return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard };
}
export function closeDropdownMenu(id) {
return { type: DROPDOWN_MENU_CLOSE, id };
}

View File

@ -10,7 +10,7 @@ export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_RE
export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS';
export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL';
export function fetchFavouritedStatuses() {
export function fetchFavoritedStatuses() {
return (dispatch, getState) => {
if (!me) return;
@ -18,26 +18,26 @@ export function fetchFavouritedStatuses() {
return;
}
dispatch(fetchFavouritedStatusesRequest());
dispatch(fetchFavoritedStatusesRequest());
api(getState).get('/api/v1/favourites').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
dispatch(fetchFavoritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchFavouritedStatusesFail(error));
dispatch(fetchFavoritedStatusesFail(error));
});
};
};
export function fetchFavouritedStatusesRequest() {
export function fetchFavoritedStatusesRequest() {
return {
type: FAVOURITED_STATUSES_FETCH_REQUEST,
skipLoading: true,
};
};
export function fetchFavouritedStatusesSuccess(statuses, next) {
export function fetchFavoritedStatusesSuccess(statuses, next) {
return {
type: FAVOURITED_STATUSES_FETCH_SUCCESS,
statuses,
@ -46,7 +46,7 @@ export function fetchFavouritedStatusesSuccess(statuses, next) {
};
};
export function fetchFavouritedStatusesFail(error) {
export function fetchFavoritedStatusesFail(error) {
return {
type: FAVOURITED_STATUSES_FETCH_FAIL,
error,
@ -54,7 +54,7 @@ export function fetchFavouritedStatusesFail(error) {
};
};
export function expandFavouritedStatuses() {
export function expandFavoritedStatuses() {
return (dispatch, getState) => {
if (!me) return;
@ -64,25 +64,25 @@ export function expandFavouritedStatuses() {
return;
}
dispatch(expandFavouritedStatusesRequest());
dispatch(expandFavoritedStatusesRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
dispatch(expandFavoritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandFavouritedStatusesFail(error));
dispatch(expandFavoritedStatusesFail(error));
});
};
};
export function expandFavouritedStatusesRequest() {
export function expandFavoritedStatusesRequest() {
return {
type: FAVOURITED_STATUSES_EXPAND_REQUEST,
};
};
export function expandFavouritedStatusesSuccess(statuses, next) {
export function expandFavoritedStatusesSuccess(statuses, next) {
return {
type: FAVOURITED_STATUSES_EXPAND_SUCCESS,
statuses,
@ -90,7 +90,7 @@ export function expandFavouritedStatusesSuccess(statuses, next) {
};
};
export function expandFavouritedStatusesFail(error) {
export function expandFavoritedStatusesFail(error) {
return {
type: FAVOURITED_STATUSES_EXPAND_FAIL,
error,

View File

@ -1,43 +0,0 @@
import api from '../api';
import { importFetchedStatuses } from './importer';
import { me } from '../initial_state';
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
export function fetchPinnedStatuses() {
return (dispatch, getState) => {
if (!me) return;
dispatch(fetchPinnedStatusesRequest());
api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
dispatch(importFetchedStatuses(response.data));
dispatch(fetchPinnedStatusesSuccess(response.data, null));
}).catch(error => {
dispatch(fetchPinnedStatusesFail(error));
});
};
};
export function fetchPinnedStatusesRequest() {
return {
type: PINNED_STATUSES_FETCH_REQUEST,
};
};
export function fetchPinnedStatusesSuccess(statuses, next) {
return {
type: PINNED_STATUSES_FETCH_SUCCESS,
statuses,
next,
};
};
export function fetchPinnedStatusesFail(error) {
return {
type: PINNED_STATUSES_FETCH_FAIL,
error,
};
};

View File

@ -0,0 +1,18 @@
export const POPOVER_OPEN = 'POPOVER_OPEN'
export const POPOVER_CLOSE = 'POPOVER_CLOSE'
export function openPopover(type, keyboard = false, placement = 'top') {
return {
keyboard,
placement,
type: POPOVER_OPEN,
popoverType: type,
}
}
export function closePopover(type) {
return {
type: POPOVER_CLOSE,
popoverType: type,
}
}

View File

@ -1,6 +1,6 @@
import { Fragment } from 'react'
import ImmutablePropTypes from 'react-immutable-proptypes';
import classNames from 'classnames';
import classNames from 'classnames/bind'
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
import { isRtl } from '../../utils/rtl';
@ -8,6 +8,8 @@ import { textAtCursorMatchesToken } from '../../utils/cursor_token_match';
import AutosuggestAccount from '../autosuggest_account';
import AutosuggestEmoji from '../autosuggest_emoji';
const cx = classNames.bind(_s)
export default class AutosuggestTextbox extends ImmutablePureComponent {
static propTypes = {
@ -30,6 +32,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
onFocus: PropTypes.func,
onBlur: PropTypes.func,
textarea: PropTypes.bool,
small: PropTypes.bool,
};
static defaultProps = {
@ -188,7 +191,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
}
render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children, className, id, maxLength, textarea } = this.props;
const { value, small, suggestions, disabled, placeholder, onKeyUp, autoFocus, children, className, id, maxLength, textarea } = this.props;
const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' };
@ -196,14 +199,30 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
style.direction = 'rtl';
}
const textClasses = cx({
default: 1,
lineHeight125: 1,
resizeNone: 1,
text: 1,
displayBlock: 1,
outlineNone: 1,
backgroundColorPrimary: !small,
backgroundSubtle: small,
paddingVertical15PX: !small,
paddingVertical10PX: small,
fontSize16PX: !small,
fontSize14PX: small,
marginRight5PX: small,
})
if (textarea) {
return (
<Fragment>
<div className={[_s.default].join(' ')}>
<div className={[_s.default, _s.flexGrow1].join(' ')}>
<div className={[_s.default, _s.marginLeft5PX].join(' ')}>
<Textarea
inputRef={this.setTextbox}
className={[_s.default, _s.backgroundColorPrimary, _s.lineHeight125, _s.resizeNone, _s.paddingVertical15PX, _s.outlineFocusBrand, _s.fontSize16PX, _s.text, _s.displayBlock].join(' ')}
className={textClasses}
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}

View File

@ -4,7 +4,8 @@ import { Map as ImmutableMap } from 'immutable'
import { autoPlayGif } from '../initial_state'
import Image from './image'
export default class Avatar extends ImmutablePureComponent {
export default
class Avatar extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
@ -24,6 +25,7 @@ export default class Avatar extends ImmutablePureComponent {
}
handleMouseEnter = () => {
// : todo : user popover
this.setState({ hovering: true })
}

View File

@ -140,7 +140,6 @@ export default class Button extends PureComponent {
}
if (tagName === 'NavLink' && !!to) {
console.log("to: ", to)
return (
<NavLink {...options}>
{theChildren}

View File

@ -1,44 +1,120 @@
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { debounce } from 'lodash'
import classNames from 'classnames/bind'
import { openPopover, closePopover } from '../actions/popover'
import Badge from './badge'
import Icon from './icon'
export default class DisplayName extends ImmutablePureComponent {
const cx = classNames.bind(_s)
const mapDispatchToProps = dispatch => ({
openUserInfoPopover() {
dispatch(openPopover('USER_INFO'))
},
closeUserInfoPopover() {
dispatch(closePopover())
}
})
export default
@connect(null, mapDispatchToProps)
class DisplayName extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
openUserInfoPopover: PropTypes.func.isRequired,
closeUserInfoPopover: PropTypes.func.isRequired,
multiline: PropTypes.bool,
large: PropTypes.bool,
noHover: PropTypes.bool,
}
render () {
const { account, multiline } = this.props
handleMouseEnter = debounce(() => {
console.log("SHOW - USER POPOVER")
this.props.openUserInfoPopover()
}, 50, { leading: true })
handleMouseLeave = () => {
console.log("HIDE - USER POPOVER")
// this.props.closeUserInfoPopover()
}
render() {
const { account, multiline, large, noHover } = this.props
if (!account) return null
const containerOptions = {
className: cx({
default: 1,
maxWidth100PC: 1,
alignItemsCenter: !multiline,
flexRow: !multiline,
cursorPointer: !noHover,
}),
onMouseEnter: noHover ? undefined : this.handleMouseEnter,
onMouseLeave: noHover ? undefined : this.handleMouseLeave,
}
const displayNameClasses = cx({
text: 1,
overflowWrapBreakWord: 1,
whiteSpaceNoWrap: 1,
fontWeightBold: 1,
colorPrimary: 1,
lineHeight125: 1,
marginRight2PX: 1,
fontSize15PX: !large,
fontSize24PX: large,
})
const usernameClasses = cx({
text: 1,
displayFlex: 1,
flexNormal: 1,
flexShrink1: 1,
overflowWrapBreakWord: 1,
textOverflowEllipsis: 1,
colorSecondary: 1,
fontWeightNormal: 1,
lineHeight15: multiline,
lineHeight125: !multiline,
marginLeft5PX: !multiline,
fontSize15PX: !large,
fontSize16PX: large
})
const iconSize = !!large ? '19px' : '16px'
// : todo :
return (
<span className={[_s.default, _s.flexRow, _s.maxWidth100PC, _s.alignItemsCenter].join(' ')}>
<bdi className={[_s.text, _s.whiteSpaceNoWrap, _s.textOverflowEllipsis].join(' ')}>
<strong
className={[_s.text, _s.overflowWrapBreakWord, _s.whiteSpaceNoWrap, _s.fontSize15PX, _s.fontWeightBold, _s.colorPrimary, _s.lineHeight125, _s.marginRight2PX].join(' ')}
dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
/>
</bdi>
{
account.get('is_verified') &&
<Icon id='verified' width='16px' height='16px' className={_s.default} title='Verified Account' />
}
{ /*
account.get('is_pro') &&
<Icon id='verified' width='16px' height='16px' className={_s.default} title='Gab PRO' />
*/ }
{ /*
account.get('is_donor') &&
<Icon id='verified' width='16px' height='16px' className={_s.default} title='Gab Donor' />
*/ }
{ /*
account.get('is_investor') &&
<Icon id='verified' width='16px' height='16px' className={_s.default} title='Gab Investor' />
*/ }
<span className={[_s.text, _s.displayFlex, _s.flexNormal, _s.flexShrink1, _s.fontSize15PX, _s.overflowWrapBreakWord, _s.textOverflowEllipsis, _s.marginLeft5PX, _s.colorSecondary, _s.fontWeightNormal, _s.lineHeight125].join(' ')}>
<span {...containerOptions}>
<div className={[_s.default, _s.flexRow, _s.alignItemsCenter].join(' ')}>
<bdi className={[_s.text, _s.whiteSpaceNoWrap, _s.textOverflowEllipsis].join(' ')}>
<strong
className={displayNameClasses}
dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
/>
</bdi>
{
account.get('is_verified') &&
<Icon id='verified' width={iconSize} height={iconSize} className={_s.default} title='Verified Account' />
}
{ /*
account.get('is_pro') &&
<Icon id='verified' width='16px' height='16px' className={_s.default} title='Gab PRO' />
*/ }
{ /*
account.get('is_donor') &&
<Icon id='verified' width='16px' height='16px' className={_s.default} title='Gab Donor' />
*/ }
{ /*
account.get('is_investor') &&
<Icon id='verified' width='16px' height='16px' className={_s.default} title='Gab Investor' />
*/ }
</div>
<span className={usernameClasses}>
@{account.get('acct')}
</span>
</span>

View File

@ -1,297 +0,0 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import detectPassiveEvents from 'detect-passive-events';
import Overlay from 'react-overlays/lib/Overlay';
import spring from 'react-motion/lib/spring';
import { openDropdownMenu, closeDropdownMenu } from '../../actions/dropdown_menu';
import { openModal, closeModal } from '../../actions/modal';
import { isUserTouching } from '../../utils/is_mobile';
import Motion from '../../features/ui/util/optional_motion'
import IconButton from '../icon_button';
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
let id = 0;
const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS',
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
});
const mapDispatchToProps = (dispatch, { status, items }) => ({
onOpen(id, onItemClick, dropdownPlacement, keyboard) {
dispatch(isUserTouching() ? openModal('ACTIONS', {
status,
actions: items,
onClick: onItemClick,
}) : openDropdownMenu(id, dropdownPlacement, keyboard));
},
onClose(id) {
dispatch(closeModal());
dispatch(closeDropdownMenu(id));
},
});
class DropdownMenu extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
items: PropTypes.array.isRequired,
onClose: PropTypes.func.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
openedViaKeyboard: PropTypes.bool,
};
static defaultProps = {
style: {},
placement: 'bottom',
};
state = {
mounted: false,
};
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus();
this.setState({ mounted: true });
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('keydown', this.handleKeyDown, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
setFocusRef = c => {
this.focusedItem = c;
}
handleKeyDown = e => {
const items = Array.from(this.node.getElementsByTagName('a'));
const index = items.indexOf(document.activeElement);
let element;
switch(e.key) {
case 'ArrowDown':
element = items[index+1];
if (element) element.focus();
break;
case 'ArrowUp':
element = items[index-1];
if (element) element.focus();
break;
case 'Home':
element = items[0];
if (element) element.focus();
break;
case 'End':
element = items[items.length-1];
if (element) element.focus();
break;
}
}
handleItemKeyDown = e => {
if (e.key === 'Enter') {
this.handleClick(e);
}
}
handleClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
this.props.onClose();
if (typeof action === 'function') {
e.preventDefault();
action(e);
} else if (to) {
e.preventDefault();
this.context.router.history.push(to);
}
}
renderItem (option, i) {
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, href = '#', newTab, isLogout } = option;
return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<a
href={href}
role='button'
tabIndex='0'
ref={i === 0 ? this.setFocusRef : null}
onClick={this.handleClick}
onKeyDown={this.handleItemKeyDown}
data-index={i}
target={newTab ? '_blank' : null}
data-method={isLogout ? 'delete' : null}
>
{text}
</a>
</li>
);
}
render () {
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
const { mounted } = this.state;
return (
<Motion defaultStyle={{ opacity: 0, scaleX: 1, scaleY: 1 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ opacity, scaleX, scaleY }) => (
// It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by
// react-overlays
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<ul>
{items.map((option, i) => this.renderItem(option, i))}
</ul>
</div>
)}
</Motion>
);
}
}
export default
@connect(mapStateToProps, mapDispatchToProps)
class Dropdown extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.string,
disabled: PropTypes.bool,
status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
dropdownPlacement: PropTypes.string,
openDropdownId: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
};
static defaultProps = {
title: 'Menu',
};
state = {
id: id++,
};
handleClick = ({ target, type }) => {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
} else {
const { top } = target.getBoundingClientRect();
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
}
}
handleClose = () => {
this.props.onClose(this.state.id);
}
handleKeyDown = e => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleClick(e);
e.preventDefault();
break;
case 'Escape':
this.handleClose();
break;
}
}
handleItemClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
this.handleClose();
if (typeof action === 'function') {
e.preventDefault();
action();
} else if (to) {
e.preventDefault();
this.context.router.history.push(to);
}
}
setTargetRef = c => {
this.target = c;
}
findTarget = () => {
return this.target;
}
componentWillUnmount = () => {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
}
}
render () {
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
const open = this.state.id === openDropdownId;
return (
<div onKeyDown={this.handleKeyDown}>
<IconButton
icon={icon}
title={title}
active={open}
disabled={disabled}
size={size}
ref={this.setTargetRef}
onClick={this.handleClick}
/>
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
<DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
</Overlay>
</div>
);
}
}

View File

@ -1,93 +0,0 @@
.dropdown-menu {
z-index: 9999;
position: absolute;
background: $gab-background-container;
padding: 4px 0;
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.5);
@include border-design($gab-placeholder-accent, 1px, 4px);
&.left {
transform-origin: 100% 50%;
}
&.top {
transform-origin: 50% 100%;
}
&.bottom {
transform-origin: 50% 0;
}
&.right {
transform-origin: 0 50%;
}
&__arrow {
position: absolute;
border: 0 solid transparent;
@include size(0);
&.left {
right: -5px;
margin-top: -5px;
border-width: 5px 0 5px 5px;
border-left-color: $gab-placeholder-accent;
}
&.top {
bottom: -5px;
margin-left: -5px;
border-width: 5px 5px 0;
border-top-color: $gab-placeholder-accent;
}
&.bottom {
top: -5px;
margin-left: -5px;
border-width: 0 5px 5px;
border-bottom-color: $gab-placeholder-accent;
}
&.right {
left: -5px;
margin-top: -5px;
border-width: 5px 5px 5px 0;
border-right-color: $gab-placeholder-accent;
}
}
ul {
overflow: hidden;
padding: 6px 0;
}
&__item a {
display: block;
box-sizing: border-box;
padding: 3px 10px 1px;
text-decoration: none;
text-transform: capitalize;
color: $gab-secondary-text;
@include text-overflow(nowrap);
@include text-sizing(13px, 400, 26px);
&:focus,
&:hover,
&:active {
outline: 0;
color: $gab-text-highlight;
background: $gab-background-base !important;
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.5);
}
}
&__separator {
display: block;
margin: 10px !important;
height: 1px;
background: $gab-background-base;
}
}

View File

@ -1 +0,0 @@
export { default } from './dropdown_menu';

View File

@ -9,6 +9,7 @@ export default class ListItem extends PureComponent {
isLast: PropTypes.bool,
to: PropTypes.string,
title: PropTypes.string,
onClick: PropTypes.func,
}
render() {

View File

@ -1,9 +1,19 @@
import ModalBase from './modal_base'
import { closeModal } from '../../actions/modal'
import { cancelReplyCompose } from '../../actions/compose'
import Bundle from '../../features/ui/util/bundle'
import {
MuteModal,
ReportModal,
EmbedModal,
// ListEditor,
// ListAdder,
StatusRevisionModal,
} from '../../features/ui/util/async-components'
import ModalBase from './modal_base'
import BundleModalError from '../bundle_modal_error'
import ActionsModal from './actions_modal'
import MediaModal from './media_modal'
// import VideoModal from './video_modal'
import VideoModal from './video_modal'
import BoostModal from './boost_modal'
import ConfirmationModal from './confirmation_modal'
import FocalPointModal from './focal_point_modal'
@ -12,35 +22,46 @@ import ComposeModal from './compose_modal'
import UnauthorizedModal from './unauthorized_modal'
import ProUpgradeModal from './pro_upgrade_modal'
import ModalLoading from './modal_loading'
import {
MuteModal,
ReportModal,
EmbedModal,
ListEditor,
ListAdder,
StatusRevisionModal,
} from '../../features/ui/util/async-components'
const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }),
// 'VIDEO': () => Promise.resolve({ default: VideoModal }),
'BOOST': () => Promise.resolve({ default: BoostModal }),
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
'MUTE': MuteModal,
'REPORT': ReportModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,
'LIST_EDITOR': ListEditor,
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
'LIST_ADDER': ListAdder,
'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }),
'STATUS_REVISION': StatusRevisionModal,
'BOOST': () => Promise.resolve({ default: BoostModal }),
'COMPOSE': () => Promise.resolve({ default: ComposeModal }),
'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }),
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
'EMBED': EmbedModal,
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }),
'MEDIA': () => Promise.resolve({ default: MediaModal }),
'MUTE': MuteModal,
'PRO_UPGRADE': () => Promise.resolve({ default: ProUpgradeModal }),
'REPORT': ReportModal,
'STATUS_REVISION': StatusRevisionModal,
'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }),
'VIDEO': () => Promise.resolve({ default: VideoModal }),
// 'LIST_EDITOR': ListEditor,
// 'LIST_ADDER': ListAdder,
// group create
// group members
}
export default class ModalRoot extends PureComponent {
const mapStateToProps = state => ({
type: state.get('modal').modalType,
props: state.get('modal').modalProps,
})
const mapDispatchToProps = (dispatch) => ({
onClose (optionalType) {
if (optionalType === 'COMPOSE') {
dispatch(cancelReplyCompose())
}
dispatch(closeModal())
},
})
export default
@connect(mapStateToProps, mapDispatchToProps)
class ModalRoot extends PureComponent {
static propTypes = {
type: PropTypes.string,

View File

@ -0,0 +1,31 @@
import { defineMessages, injectIntl } from 'react-intl'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { me } from '../../initial_state'
import { shortNumberFormat } from '../../utils/numbers'
import PanelLayout from './panel_layout'
import UserStat from '../user_stat'
const messages = defineMessages({
title: { id: 'about', defaultMessage: 'About' },
})
export default
@injectIntl
class GroupInfoPanel extends ImmutablePureComponent {
static propTypes = {
group: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
}
render() {
const { intl, group } = this.props
return (
<PanelLayout title={intl.formatMessage(messages.title)}>
</PanelLayout>
)
}
}

View File

@ -1,71 +1,53 @@
import { defineMessages, injectIntl } from 'react-intl';
import { fetchSuggestions, dismissSuggestion } from '../../actions/suggestions';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import AccountContainer from '../../containers/account_container';
import PanelLayout from './panel_layout';
import { defineMessages, injectIntl } from 'react-intl'
import { fetchSuggestions, dismissSuggestion } from '../../actions/suggestions'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import AccountContainer from '../../containers/account_container'
import PanelLayout from './panel_layout'
const messages = defineMessages({
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
title: { id: 'who_to_follow.title', defaultMessage: 'Who to Follow' },
show_more: { id: 'who_to_follow.more', defaultMessage: 'Show more' },
});
title: { id: 'media_gallery_panel.title', defaultMessage: 'Media' },
show_all: { id: 'media_gallery_panel.all', defaultMessage: 'Show all' },
})
const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions', 'items']),
});
})
const mapDispatchToProps = dispatch => {
return {
fetchSuggestions: () => dispatch(fetchSuggestions()),
dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
}
};
}
export default
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class WhoToFollowPanel extends ImmutablePureComponent {
class MediaGalleryPanel extends ImmutablePureComponent {
static propTypes = {
suggestions: ImmutablePropTypes.list.isRequired,
fetchSuggestions: PropTypes.func.isRequired,
dismissSuggestion: PropTypes.func.isRequired,
account: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
};
}
componentDidMount () {
this.props.fetchSuggestions();
componentDidMount() {
// this.props.fetchSuggestions()
}
render() {
const { intl, /* suggestions, */ dismissSuggestion } = this.props;
// : testing!!! :
const suggestions = [
"1",
]
// if (suggestions.isEmpty()) {
// return null;
// }
const { intl, account } = this.props
console.log("account:", account)
return (
<PanelLayout
title={intl.formatMessage(messages.title)}
footerButtonTitle={intl.formatMessage(messages.show_more)}
footerButtonTo='/explore'
headerButtonTitle={intl.formatMessage(messages.show_all)}
headerButtonTo='/explore'
>
<div className={_s.default}>
{suggestions && suggestions.map(accountId => (
<AccountContainer
key={accountId}
id={accountId}
actionIcon='times'
actionTitle={intl.formatMessage(messages.dismissSuggestion)}
onActionClick={dismissSuggestion}
/>
))}
</div>
</PanelLayout>
);
};
};
)
}
}

View File

@ -1,71 +1,157 @@
import { defineMessages, injectIntl } from 'react-intl';
import { fetchSuggestions, dismissSuggestion } from '../../actions/suggestions';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import AccountContainer from '../../containers/account_container';
import PanelLayout from './panel_layout';
import { defineMessages, injectIntl } from 'react-intl'
import { fetchSuggestions, dismissSuggestion } from '../../actions/suggestions'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { List as ImmutableList } from 'immutable'
import classNames from 'classnames/bind'
import PanelLayout from './panel_layout'
import Icon from '../icon'
import Text from '../text'
const cx = classNames.bind(_s)
const messages = defineMessages({
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
title: { id: 'who_to_follow.title', defaultMessage: 'Who to Follow' },
show_more: { id: 'who_to_follow.more', defaultMessage: 'Show more' },
});
title: { id: 'about', defaultMessage: 'About' },
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
bot: { id: 'account.badges.bot', defaultMessage: 'Bot' },
memberSince: { id: 'account.member_since', defaultMessage: 'Member since {date}' },
})
const mapStateToProps = (state, { account }) => {
const identityProofs = !!account ? state.getIn(['identity_proofs', account.get('id')], ImmutableList()) : ImmutableList();
return {
identityProofs,
domain: state.getIn(['meta', 'domain']),
};
};
const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions', 'items']),
});
const mapDispatchToProps = dispatch => {
return {
fetchSuggestions: () => dispatch(fetchSuggestions()),
dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
}
};
}
export default
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class WhoToFollowPanel extends ImmutablePureComponent {
class ProfileInfoPanel extends ImmutablePureComponent {
static propTypes = {
suggestions: ImmutablePropTypes.list.isRequired,
fetchSuggestions: PropTypes.func.isRequired,
dismissSuggestion: PropTypes.func.isRequired,
identityProofs: ImmutablePropTypes.list,
account: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
};
}
componentDidMount () {
this.props.fetchSuggestions();
componentDidMount() {
this.props.fetchSuggestions()
}
render() {
const { intl, /* suggestions, */ dismissSuggestion } = this.props;
// : testing!!! :
const suggestions = [
"1",
]
// if (suggestions.isEmpty()) {
// return null;
// }
const { intl, account, identityProofs } = this.props
const fields = !account ? null : account.get('fields')
const content = !account ? null : { __html: account.get('note_emojified') }
const memberSinceDate = !account ? null : intl.formatDate(account.get('created_at'), { month: 'long', year: 'numeric' });
const hasNote = !!content ? (account.get('note').length > 0 && account.get('note') !== '<p></p>') : false
const lineClasses = cx({
default: 1,
flexRow: 1,
alignItemsCenter: 1,
borderTop1PX: 1,
borderColorSecondary: 1,
marginTop10PX: 1,
paddingTop10PX: 1,
})
const memberLineClasses = cx({
default: 1,
flexRow: 1,
alignItemsCenter: 1,
borderTop1PX: hasNote,
borderColorSecondary: hasNote,
marginTop10PX: hasNote,
paddingTop10PX: hasNote,
})
return (
<PanelLayout
title={intl.formatMessage(messages.title)}
footerButtonTitle={intl.formatMessage(messages.show_more)}
footerButtonTo='/explore'
>
<div className={_s.default}>
{suggestions && suggestions.map(accountId => (
<AccountContainer
key={accountId}
id={accountId}
actionIcon='times'
actionTitle={intl.formatMessage(messages.dismissSuggestion)}
onActionClick={dismissSuggestion}
/>
))}
</div>
<PanelLayout title={intl.formatMessage(messages.title)}>
{
!!account &&
<div className={[_s.default].join(' ')}>
{
hasNote &&
<div className={_s.dangerousContent} dangerouslySetInnerHTML={content} />
}
<div className={memberLineClasses}>
<Icon id='calendar' width='12px' height='12px' className={_s.fillcolorSecondary} />
<Text
size='small'
color='secondary'
className={_s.marginLeft5PX}
>
{
intl.formatMessage(messages.memberSince, {
date: memberSinceDate
})
}
</Text>
</div>
{(fields.size > 0 || identityProofs.size > 0) && (
<div className={[_s.default]}>
{identityProofs.map((proof, i) => (
<dl className={lineClasses} key={`profile-identity-proof-${i}`}>
<dt
className={_s.dangerousContent}
dangerouslySetInnerHTML={{ __html: proof.get('provider') }}
/>
{ /* : todo : */ }
<dd className='verified'>
<a href={proof.get('proof_url')} target='_blank' rel='noopener'>
<span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
<Icon id='check' className='verified__mark' />
</span>
</a>
<a href={proof.get('profile_url')} target='_blank' rel='noopener'>
<span
className={_s.dangerousContent}
dangerouslySetInnerHTML={{ __html: ' ' + proof.get('provider_username') }}
/>
</a>
</dd>
</dl>
))}
{
fields.map((pair, i) => (
<dl className={lineClasses} key={`profile-field-${i}`}>
<dt
className={[_s.text, _s.dangerousContent].join('')}
dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }}
title={pair.get('name')}
/>
<dd
className={[_s.dangerousContent, _s.marginLeftAuto].join(' ')}
title={pair.get('value_plain')}
dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }}
/>
</dl>
))
}
</div>
)}
</div>
}
</PanelLayout>
);
};
};
)
}
}

View File

@ -0,0 +1,63 @@
import { defineMessages, injectIntl } from 'react-intl'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { me } from '../../initial_state'
import { shortNumberFormat } from '../../utils/numbers'
import PanelLayout from './panel_layout'
import UserStat from '../user_stat'
const messages = defineMessages({
gabs: { id: 'account.gabs', defaultMessage: 'Gabs' },
followers: { id: 'account.followers', defaultMessage: 'Followers' },
follows: { id: 'account.follows', defaultMessage: 'Follows' },
favorites: { id: 'navigation_bar.favorites', defaultMessage: 'Favorites' },
})
export default
@injectIntl
class ProfileStatsPanel extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
}
render() {
const { intl, account } = this.props
if (!account) return null
return (
<PanelLayout>
{
!!account &&
<div className={[_s.default, _s.flexRow].join(' ')}>
<UserStat
title={intl.formatMessage(messages.gabs)}
value={shortNumberFormat(account.get('statuses_count'))}
to={`/${account.get('acct')}`}
/>
<UserStat
title={intl.formatMessage(messages.follows)}
value={shortNumberFormat(account.get('following_count'))}
to={`/${account.get('acct')}/following`}
/>
<UserStat
title={intl.formatMessage(messages.followers)}
value={shortNumberFormat(account.get('followers_count'))}
to={`/${account.get('acct')}/followers`}
/>
{
account.get('id') === me &&
<UserStat
title={intl.formatMessage(messages.favorites)}
value={shortNumberFormat(account.get('favourite_count'))}
to={`/${account.get('acct')}/favorites`}
/>
}
</div>
}
</PanelLayout>
)
}
}

View File

@ -2,7 +2,7 @@ import { NavLink } from 'react-router-dom'
import { injectIntl, defineMessages } from 'react-intl'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { autoPlayGif, me } from '../../initial_state'
import { me } from '../../initial_state'
import { makeGetAccount } from '../../selectors'
import { shortNumberFormat } from '../../utils/numbers'
import DisplayName from '../display_name'
@ -18,10 +18,8 @@ const messages = defineMessages({
})
const mapStateToProps = state => {
const getAccount = makeGetAccount()
return {
account: getAccount(state, me),
account: makeGetAccount()(state, me),
}
}
@ -36,25 +34,6 @@ class UserPanel extends ImmutablePureComponent {
render() {
const { account, intl } = this.props
const displayNameHtml = { __html: account.get('display_name_html') }
const statItems = [
{
to: `/${account.get('acct')}`,
title: intl.formatMessage(messages.gabs),
value: shortNumberFormat(account.get('statuses_count')),
},
{
to: `/${account.get('acct')}/followers`,
title: intl.formatMessage(messages.followers),
value: shortNumberFormat(account.get('followers_count')),
},
{
to: `/${account.get('acct')}/following`,
title: intl.formatMessage(messages.follows),
value: shortNumberFormat(account.get('following_count')),
},
]
return (
<PanelLayout noPadding>
@ -76,11 +55,21 @@ class UserPanel extends ImmutablePureComponent {
</NavLink>
<div className={[_s.default, _s.marginBottom15PX, _s.marginTop5PX, _s.flexRow, _s.paddingHorizontal15PX].join(' ')}>
{
statItems.map((statItem, i) => (
<UserStat {...statItem} key={`user-stat-panel-item-${i}`} />
))
}
<UserStat
to={`/${account.get('acct')}`}
title={intl.formatMessage(messages.gabs)}
value={shortNumberFormat(account.get('statuses_count'))}
/>
<UserStat
to={`/${account.get('acct')}/followers`}
title={intl.formatMessage(messages.followers)}
value={shortNumberFormat(account.get('followers_count'))}
/>
<UserStat
to={`/${account.get('acct')}/following`}
title={intl.formatMessage(messages.follows)}
value={shortNumberFormat(account.get('following_count'))}
/>
</div>
</PanelLayout>
)

View File

@ -1,4 +1,4 @@
export default class Popover extends PureComponent {
export default class ContentWarningPopover extends PureComponent {
render() {
return (
<div>

View File

@ -1,4 +1,4 @@
export default class Popover extends PureComponent {
export default class DatePickerPopover extends PureComponent {
render() {
return (
<div>

View File

@ -0,0 +1,12 @@
import PopoverLayout from './popover_layout'
import Text from '../text'
export default class UserInfoPopover extends PureComponent {
render() {
return (
<PopoverLayout>
<Text>testing</Text>
</PopoverLayout>
)
}
}

View File

@ -1,9 +0,0 @@
export default class Popover extends PureComponent {
render() {
return (
<div>
{ /* */ }
</div>
)
}
}

View File

@ -0,0 +1,146 @@
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import classnames from 'classnames/bind'
import Overlay from 'react-overlays/lib/Overlay'
import spring from 'react-motion/lib/spring'
import Motion from '../../features/ui/util/optional_motion'
import { openPopover, closePopover } from '../../actions/popover'
import { openModal, closeModal } from '../../actions/modal'
import { isUserTouching } from '../../utils/is_mobile'
const cx = classnames.bind(_s)
let id = 0
const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS',
popoverPlacement: state.getIn(['popover', 'placement']),
openPopoverType: state.getIn(['popover', 'popoverType']),
openedViaKeyboard: state.getIn(['popover', 'keyboard']),
})
const mapDispatchToProps = (dispatch, { status, items }) => ({
onOpen(id, onItemClick, popoverPlacement, keyboard) {
dispatch(isUserTouching() ? openModal('ACTIONS', {
status,
actions: items,
onClick: onItemClick,
}) : openPopover(id, popoverPlacement, keyboard))
},
onClose(id) {
dispatch(closeModal())
dispatch(closePopover(id))
},
})
export default
@connect(mapStateToProps, mapDispatchToProps)
class PopoverBase extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
}
static propTypes = {
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.string,
disabled: PropTypes.bool,
status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
popoverPlacement: PropTypes.string,
openPopoverType: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
visible: PropTypes.bool,
}
static defaultProps = {
title: 'Menu',
}
state = {
id: id++,
}
handleClick = ({ target, type }) => {
if (this.state.id === this.props.openPopoverType) {
this.handleClose()
} else {
const { top } = target.getBoundingClientRect()
const placement = top * 2 < innerHeight ? 'bottom' : 'top'
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click')
}
}
handleClose = () => {
this.props.onClose(this.state.id)
}
handleKeyDown = e => {
switch (e.key) {
case ' ':
case 'Enter':
this.handleClick(e)
e.preventDefault()
break
case 'Escape':
this.handleClose()
break
}
}
handleItemClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'))
const { action, to } = this.props.items[i]
this.handleClose()
if (typeof action === 'function') {
e.preventDefault()
action()
} else if (to) {
e.preventDefault()
this.context.router.history.push(to)
}
}
setTargetRef = c => {
this.target = c
}
findTarget = () => {
return this.target
}
componentWillUnmount = () => {
if (this.state.id === this.props.openPopoverType) {
this.handleClose()
}
}
render() {
const { icon, children, visible, items, size, title, disabled, popoverPlacement, openPopoverType, openedViaKeyboard } = this.props
const open = this.state.id === openPopoverType
const containerClasses = cx({
default: 1,
z4: 1,
displayNone: !visible,
})
return (
<div onKeyDown={this.handleKeyDown} className={containerClasses}>
<div show={open} placement={popoverPlacement} target={this.findTarget}>
{ /* <PopoverMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} /> */}
{children}
</div>
</div>
)
}
}

View File

@ -1,8 +1,14 @@
export default class Popover extends PureComponent {
import Block from '../block'
export default class PopoverLayout extends PureComponent {
render() {
const { children } = this.props
return (
<div>
{ /* */ }
<Block>
{children}
</Block>
</div>
)
}

View File

@ -0,0 +1,171 @@
import detectPassiveEvents from 'detect-passive-events'
import { closePopover } from '../../actions/popover'
import Bundle from '../../features/ui/util/bundle'
import BundleModalError from '../bundle_modal_error'
import PopoverBase from './popover_base'
import ContentWarningPopover from './content_warning_popover'
import DatePickerPopover from './date_picker_popover'
import GroupInfoPopover from './group_info_popover'
import ProfileOptionsPopover from './profile_options_popover'
import SearchPopover from './search_popover'
import SidebarMorePopover from './sidebar_more_popover'
import StatusOptionsPopover from './status_options_popover'
import UserInfoPopover from './user_info_popover'
import StatusVisibilityPopover from './status_visibility_popover'
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false
const POPOVER_COMPONENTS = {
CONTENT_WARNING: () => Promise.resolve({ default: ContentWarningPopover }),
DATE_PICKER: () => Promise.resolve({ default: DatePickerPopover }),
GROUP_INFO: () => GroupInfoPopover,
PROFILE_OPTIONS: () => ProfileOptionsPopover,
SEARCH: () => Promise.resolve({ default: SearchPopover }),
SIDEBAR_MORE: () => Promise.resolve({ default: SidebarMorePopover }),
STATUS_OPTIONS: () => Promise.resolve({ default: StatusOptionsPopover }),
STATUS_VISIBILITY: () => Promise.resolve({ default: StatusVisibilityPopover }),
USER_INFO: () => UserInfoPopover,
}
const mapStateToProps = state => ({
type: state.get('popover').popoverType,
props: state.get('popover').popoverProps,
})
const mapDispatchToProps = (dispatch) => ({
onClose(optionalType) {
//
dispatch(closePopover())
},
})
export default
@connect(mapStateToProps, mapDispatchToProps)
class PopoverRoot extends PureComponent {
static propTypes = {
type: PropTypes.string,
props: PropTypes.object,
onClose: PropTypes.func.isRequired,
}
getSnapshotBeforeUpdate() {
return { visible: !!this.props.type }
}
static contextTypes = {
router: PropTypes.object,
}
static propTypes = {
onClose: PropTypes.func.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
openedViaKeyboard: PropTypes.bool,
}
static defaultProps = {
style: {},
placement: 'bottom',
}
state = {
mounted: false,
}
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose()
}
}
componentDidMount() {
document.addEventListener('click', this.handleDocumentClick, false)
document.addEventListener('keydown', this.handleKeyDown, false)
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions)
if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus()
this.setState({ mounted: true })
}
componentWillUnmount() {
document.removeEventListener('click', this.handleDocumentClick, false)
document.removeEventListener('keydown', this.handleKeyDown, false)
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions)
}
setRef = c => {
this.node = c
}
setFocusRef = c => {
this.focusedItem = c
}
handleKeyDown = e => {
const items = Array.from(this.node.getElementsByTagName('a'))
const index = items.indexOf(document.activeElement)
let element
switch (e.key) {
case 'ArrowDown':
element = items[index + 1]
if (element) element.focus()
break
case 'ArrowUp':
element = items[index - 1]
if (element) element.focus()
break
case 'Home':
element = items[0]
if (element) element.focus()
break
case 'End':
element = items[items.length - 1]
if (element) element.focus()
break
}
}
handleItemKeyDown = e => {
if (e.key === 'Enter') {
this.handleClick(e)
}
}
handleClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'))
const { action, to } = this.props.items[i]
this.props.onClose()
if (typeof action === 'function') {
e.preventDefault()
action(e)
} else if (to) {
e.preventDefault()
this.context.router.history.push(to)
}
}
renderError = (props) => {
return <BundleModalError />
}
render() {
const { type, style, placement } = this.props
const { mounted } = this.state
const visible = !!type
console.log("popover root - type, visible:", type, visible)
return (
<PopoverBase className={`popover-menu ${placement}`} visible={visible} ref={this.setRef}>
{
visible &&
<UserInfoPopover />
}
</PopoverBase>
)
}
}

View File

@ -0,0 +1,12 @@
import PopoverLayout from './popover_layout'
import Text from '../text'
export default class UserInfoPopover extends PureComponent {
render() {
return (
<PopoverLayout>
<Text>testing</Text>
</PopoverLayout>
)
}
}

View File

@ -1,4 +1,4 @@
export default class Popover extends PureComponent {
export default class SearchPopover extends PureComponent {
render() {
return (
<div>

View File

@ -1,4 +1,4 @@
export default class Popover extends PureComponent {
export default class SidebarMorePopover extends PureComponent {
render() {
return (
<div>

View File

@ -1,4 +1,4 @@
export default class Popover extends PureComponent {
export default class StatusOptionsPopover extends PureComponent {
render() {
return (
<div>

View File

@ -1,4 +1,4 @@
export default class Popover extends PureComponent {
export default class StatusVisibilityPopover extends PureComponent {
render() {
return (
<div>

View File

@ -0,0 +1,12 @@
import PopoverLayout from './popover_layout'
import Text from '../text'
export default class UserInfoPopover extends PureComponent {
render() {
return (
<PopoverLayout>
<Text>testing</Text>
</PopoverLayout>
)
}
}

View File

@ -1,9 +0,0 @@
export default class Popover extends PureComponent {
render() {
return (
<div>
{ /* */ }
</div>
)
}
}

View File

@ -45,7 +45,7 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(closeSidebar())
},
onOpenComposeModal() {
dispatch(openModal('PRO_UPGRADE'))
dispatch(openModal('COMPOSE'))
},
})
@ -98,6 +98,7 @@ class Sidebar extends ImmutablePureComponent {
const { sidebarOpen, intl, account } = this.props
const { moreOpen } = this.state
// : todo :
if (!me || !account) return null
const acct = account.get('acct')
@ -124,11 +125,6 @@ class Sidebar extends ImmutablePureComponent {
to: '/notifications',
count: 40,
},
{
title: 'Bookmarks',
icon: 'bookmarks',
to: '/bookmarks',
},
{
title: 'Groups',
icon: 'group',
@ -142,7 +138,8 @@ class Sidebar extends ImmutablePureComponent {
{
title: 'Chat',
icon: 'chat',
to: '/',
to: '',
// href: 'https://chat.gab.com',
},
{
title: 'More',
@ -175,22 +172,26 @@ class Sidebar extends ImmutablePureComponent {
{
title: 'Apps',
icon: 'apps',
to: '/',
to: '',
// href: 'https://apps.gab.com',
},
{
title: 'Shop',
icon: 'shop',
to: '/',
to: '',
// href: 'https://shop.dissenter.com',
},
{
title: 'Trends',
icon: 'trends',
to: '/',
to: '',
// href: 'https://trends.gab.com',
},
{
title: 'Dissenter',
icon: 'dissenter',
to: '/',
to: '',
// href: 'https://dissenter.com',
},
]

View File

@ -15,7 +15,8 @@ const mapStateToProps = state => {
}
}
export default @connect(mapStateToProps)
export default
@connect(mapStateToProps)
class SidebarHeader extends ImmutablePureComponent {
static propTypes = {

View File

@ -1,10 +1,10 @@
import { Fragment } from 'react'
import { NavLink } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage } from 'react-intl';
import { injectIntl, defineMessages } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames';
import classNames from 'classnames/bind'
import { displayMedia } from '../../initial_state';
import Card from '../../features/status/components/card';
import { MediaGallery, Video } from '../../features/ui/util/async-components';
@ -18,11 +18,14 @@ import Block from '../block';
import Icon from '../icon';
import Poll from '../poll';
import StatusHeader from '../status_header'
import Text from '../text'
// 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/util/bundle';
const cx = classNames.bind(_s)
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']);
@ -52,6 +55,12 @@ export const defaultMediaVisibility = status => {
return (displayMedia !== 'hide_all' && !status.get('sensitive')) || displayMedia === 'show_all';
};
const messages = defineMessages({
filtered: { id: 'status.filtered', defaultMessage: 'Filtered' },
promoted: { id:'status.promoted', defaultMessage: 'Promoted gab' },
pinned: { id: 'status.pinned', defaultMessage: 'Pinned gab' },
})
export default
@injectIntl
class Status extends ImmutablePureComponent {
@ -293,7 +302,7 @@ class Status extends ImmutablePureComponent {
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
<Text>{intl.formatMessage(messages.filtered)}</Text>
</div>
</HotKeys>
);
@ -302,17 +311,24 @@ class Status extends ImmutablePureComponent {
if (promoted) {
prepend = (
<button className='status__prepend status__prepend--promoted' onClick={this.handleOpenProUpgradeModal}>
<div className='status__prepend-icon-wrapper'><Icon id='star' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.promoted' defaultMessage='Promoted gab' />
<div className='status__prepend-icon-wrapper'>
<Icon id='star' className='status__prepend-icon' fixedWidth />
</div>
<Text>{intl.formatMessage(messages.promoted)}</Text>
</button>
);
} else if (featured) {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'>
<Icon id='thumb-tack' className='status__prepend-icon' fixedWidth />
</div>
<FormattedMessage id='status.pinned' defaultMessage='Pinned gab' />
<div className={[_s.default, _s.flexRow, _s.alignItemsCenter, _s.borderBottom1PX, _s.borderColorSecondary, _s.paddingVertical5PX, _s.paddingHorizontal15PX].join(' ')}>
<Icon
id='thumb-tack'
width='12px'
height='12px'
className={_s.fillcolorSecondary}
/>
<Text size='small' color='secondary' className={_s.marginLeft5PX}>
{intl.formatMessage(messages.pinned)}
</Text>
</div>
);
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
@ -323,7 +339,7 @@ class Status extends ImmutablePureComponent {
<div className='status__prepend-icon-wrapper'>
<Icon id='retweet' className='status__prepend-icon' fixedWidth />
</div>
<FormattedMessage
{/*<FormattedMessage
id='status.reblogged_by'
defaultMessage='{name} reposted'
values={{
@ -335,7 +351,7 @@ class Status extends ImmutablePureComponent {
</NavLink>
),
}}
/>
/> */ }
</div>
);
@ -423,10 +439,18 @@ class Status extends ImmutablePureComponent {
const statusUrl = `/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
const containerClasses = cx({
default: 1,
marginBottom15PX: 1,
paddingBottom15PX: featured,
borderBottom1PX: featured,
borderColorSecondary: featured,
})
return (
<HotKeys handlers={handlers}>
<div
className={[_s.default, _s.marginBottom15PX].join(' ')}
className={containerClasses}
tabIndex={this.props.muted ? null : 0}
data-featured={featured ? 'true' : null}
aria-label={textForScreenReader(intl, status, rebloggedByText)}

View File

@ -1,13 +1,12 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { defineMessages, injectIntl } from 'react-intl'
import classNames from 'classnames/bind'
import { Link } from 'react-router-dom';
import { openModal } from '../../actions/modal';
import { me, isStaff } from '../../initial_state';
import Dropdown from '../dropdown_menu'
import ComposeFormContainer from '../../features/compose/containers/compose_form_container';
import Icon from '../icon';
import { openModal } from '../../actions/modal'
import { me, isStaff } from '../../initial_state'
import ComposeFormContainer from '../../features/compose/containers/compose_form_container'
import Icon from '../icon'
import StatusActionBarItem from '../status_action_bar_item'
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -40,64 +39,16 @@ const messages = defineMessages({
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
group_remove_account: { id: 'status.remove_account_from_group', defaultMessage: 'Remove account from group' },
group_remove_post: { id: 'status.remove_post_from_group', defaultMessage: 'Remove status from group' },
});
})
const mapDispatchToProps = (dispatch) => ({
onOpenUnauthorizedModal() {
dispatch(openModal('UNAUTHORIZED'));
dispatch(openModal('UNAUTHORIZED'))
},
});
})
const cx = classNames.bind(_s)
class StatusActionBarItem extends PureComponent {
static propTypes = {
title: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
icon: PropTypes.string.isRequired,
active: PropTypes.bool,
disabled: PropTypes.bool,
}
render() {
const { title, onClick, icon, active, disabled } = this.props
const btnClasses = cx({
default: 1,
text: 1,
fontSize13PX: 1,
fontWeightMedium: 1,
cursorPointer: 1,
displayFlex: 1,
justifyContentCenter: 1,
flexRow: 1,
alignItemsCenter: 1,
paddingVertical10PX: 1,
paddingHorizontal10PX: 1,
width100PC: 1,
radiusSmall: 1,
outlineFocusBrand: 1,
backgroundTransparent: 1,
backgroundSubtle_onHover: 1,
colorSecondary: 1,
})
return (
<div className={[_s.default, _s.flexGrow1, _s.paddingHorizontal10PX].join(' ')}>
<button
className={btnClasses}
onClick={onClick}
active={active}
disabled={disabled}
>
<Icon width='16px' height='16px' id={icon} className={[_s.default, _s.marginRight10PX, _s.fillcolorSecondary].join(' ')} />
{title}
</button>
</div>
)
}
}
export default
@connect(null, mapDispatchToProps)
@injectIntl
@ -105,7 +56,7 @@ class StatusActionBar extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
}
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
@ -125,7 +76,7 @@ class StatusActionBar extends ImmutablePureComponent {
withDismiss: PropTypes.bool,
withGroupAdmin: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
}
// Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage.
@ -136,53 +87,53 @@ class StatusActionBar extends ImmutablePureComponent {
handleReplyClick = () => {
if (me) {
this.props.onReply(this.props.status, this.context.router.history);
this.props.onReply(this.props.status, this.context.router.history)
} else {
this.props.onOpenUnauthorizedModal();
this.props.onOpenUnauthorizedModal()
}
}
handleQuoteClick = () => {
if (me) {
this.props.onQuote(this.props.status, this.context.router.history);
this.props.onQuote(this.props.status, this.context.router.history)
} else {
this.props.onOpenUnauthorizedModal();
this.props.onOpenUnauthorizedModal()
}
}
handleFavouriteClick = () => {
if (me) {
this.props.onFavourite(this.props.status);
this.props.onFavourite(this.props.status)
} else {
this.props.onOpenUnauthorizedModal();
this.props.onOpenUnauthorizedModal()
}
}
handleReblogClick = e => {
if (me) {
this.props.onReblog(this.props.status, e);
this.props.onReblog(this.props.status, e)
} else {
this.props.onOpenUnauthorizedModal();
this.props.onOpenUnauthorizedModal()
}
}
render () {
const { status, intl: { formatMessage } } = this.props;
render() {
const { status, intl: { formatMessage } } = this.props
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'))
const replyCount = status.get('replies_count');
const replyIcon = (status.get('in_reply_to_id', null) === null) ? 'reply' : 'reply-all';
const replyTitle = (status.get('in_reply_to_id', null) === null) ? formatMessage(messages.reply) : formatMessage(messages.replyAll);
const replyCount = status.get('replies_count')
const replyIcon = (status.get('in_reply_to_id', null) === null) ? 'reply' : 'reply-all'
const replyTitle = (status.get('in_reply_to_id', null) === null) ? formatMessage(messages.reply) : formatMessage(messages.replyAll)
const reblogCount = status.get('reblogs_count');
const reblogTitle = !publicStatus ? formatMessage(messages.cannot_reblog) : formatMessage(messages.reblog);
const reblogCount = status.get('reblogs_count')
const reblogTitle = !publicStatus ? formatMessage(messages.cannot_reblog) : formatMessage(messages.reblog)
const favoriteCount = status.get('favourites_count');
const favoriteCount = status.get('favourites_count')
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
<IconButton className='status-action-bar-button' title={formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
);
)
const items = [
{
@ -248,19 +199,19 @@ class StatusActionBar extends ImmutablePureComponent {
{
hasInteractions &&
<div className={[_s.default, _s.flexRow, _s.paddingHorizontal5PX].join(' ')}>
{ favoriteCount > 0 &&
{favoriteCount > 0 &&
<button className={interactionBtnClasses}>
{favoriteCount}
&nbsp;Likes
</button>
}
{ replyCount > 0 &&
{replyCount > 0 &&
<button className={interactionBtnClasses}>
{replyCount}
&nbsp;Comments
</button>
}
{ reblogCount > 0 &&
{reblogCount > 0 &&
<button className={interactionBtnClasses}>
{reblogCount}
&nbsp;Reposts
@ -277,11 +228,11 @@ class StatusActionBar extends ImmutablePureComponent {
}
</div>
</div>
<div className='status-action-bar__comment'>
{/*<ComposeFormContainer shouldCondense statusId={status.get('id')} />*/}
<div className={[_s.default, _s.borderTop1PX, _s.borderColorSecondary, _s.paddingTop10PX, _s.marginBottom10PX].join(' ')}>
{ /* <ComposeFormContainer statusId={status.get('id')} shouldCondense /> */ }
</div>
</div>
);
)
}
}

View File

@ -0,0 +1,52 @@
import classNames from 'classnames/bind'
import Icon from './icon'
const cx = classNames.bind(_s)
export default class StatusActionBarItem extends PureComponent {
static propTypes = {
title: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
icon: PropTypes.string.isRequired,
active: PropTypes.bool,
disabled: PropTypes.bool,
}
render() {
const { title, onClick, icon, active, disabled } = this.props
const btnClasses = cx({
default: 1,
text: 1,
fontSize13PX: 1,
fontWeightMedium: 1,
cursorPointer: 1,
displayFlex: 1,
justifyContentCenter: 1,
flexRow: 1,
alignItemsCenter: 1,
paddingVertical10PX: 1,
paddingHorizontal10PX: 1,
width100PC: 1,
radiusSmall: 1,
outlineFocusBrand: 1,
backgroundTransparent: 1,
backgroundSubtle_onHover: 1,
colorSecondary: 1,
})
return (
<div className={[_s.default, _s.flexGrow1, _s.paddingHorizontal10PX].join(' ')}>
<button
className={btnClasses}
onClick={onClick}
active={active}
disabled={disabled}
>
<Icon width='16px' height='16px' id={icon} className={[_s.default, _s.marginRight10PX, _s.fillcolorSecondary].join(' ')} />
{title}
</button>
</div>
)
}
}

View File

@ -3,16 +3,17 @@ import TabBarItem from './tab_bar_item'
export default class TabBar extends PureComponent {
static propTypes = {
tabs: PropTypes.array,
large: PropTypes.bool,
}
render() {
const { tabs } = this.props
const { tabs, large } = this.props
return (
<div className={[_s.default, _s.height53PX, _s.paddingHorizontal5PX, _s.flexRow].join(' ')}>
{
{ !!tabs &&
tabs.map((tab, i) => (
<TabBarItem key={`tab-bar-item-${i}`} {...tab} />
<TabBarItem key={`tab-bar-item-${i}`} {...tab} large={large} />
))
}
</div>

View File

@ -11,6 +11,7 @@ class TabBarItem extends PureComponent {
location: PropTypes.object.isRequired,
title: PropTypes.string,
to: PropTypes.string,
large: PropTypes.bool,
}
state = {
@ -30,7 +31,7 @@ class TabBarItem extends PureComponent {
}
render() {
const { title, to, location } = this.props
const { title, to, location, large } = this.props
const { active } = this.state
const isCurrent = active === -1 ? to === location.pathname : active
@ -43,23 +44,40 @@ class TabBarItem extends PureComponent {
displayFlex: 1,
alignItemsCenter: 1,
justifyContentCenter: 1,
paddingHorizontal10PX: 1,
borderBottom2PX: 1,
paddingVertical5PX: 1,
borderColorTransparent: !isCurrent,
borderColorBrand: isCurrent,
marginRight5PX: large,
})
const textParentClasses = cx({
default: 1,
height100PC: 1,
alignItemsCenter: 1,
justifyContentCenter: 1,
radiusSmall: 1,
paddingHorizontal10PX: !large,
paddingHorizontal15PX: large,
backgroundSubtle2Dark_onHover: !isCurrent,
})
const textOptions = {
size: 'small',
color: isCurrent ? 'brand' : 'primary',
weight: isCurrent ? 'bold' : 'normal',
size: !!large ? 'normal' : 'small',
color: isCurrent ? 'brand' : large ? 'secondary' : 'primary',
weight: isCurrent ? 'bold' : large ? 'medium' : 'normal',
className: cx({
paddingHorizontal5PX: large,
}),
}
return (
<NavLink to={to} className={containerClasses}>
<Text {...textOptions}>
{title}
</Text>
<span className={textParentClasses}>
<Text {...textOptions}>
{title}
</Text>
</span>
</NavLink>
)
}

View File

@ -44,9 +44,6 @@ class TimelineComposeBlock extends ImmutablePureComponent {
</Heading>
</div>
<div className={[_s.default, _s.flexRow, _s.paddingVertical15PX, _s.paddingHorizontal15PX].join(' ')}>
<div className={[_s.default, _s.marginRight10PX].join(' ')}>
<Avatar account={account} size={46} />
</div>
<ComposeFormContainer {...rest} />
</div>
</Block>

View File

@ -1,7 +1,7 @@
import LoadingBar from 'react-redux-loading-bar';
import LoadingBar from 'react-redux-loading-bar'
const mapStateToProps = (state, ownProps) => ({
loading: state.get('loadingBar')[ownProps.scope || 'default'],
});
export default connect(mapStateToProps)(LoadingBar.WrappedComponent);
export default connect(mapStateToProps)(LoadingBar.WrappedComponent)

View File

@ -1,20 +0,0 @@
import { closeModal } from '../actions/modal';
import { cancelReplyCompose } from '../actions/compose';
import ModalRoot from '../components/modal/modal_root';
const mapStateToProps = state => ({
type: state.get('modal').modalType,
props: state.get('modal').modalProps,
});
const mapDispatchToProps = (dispatch) => ({
onClose (optionalType) {
if (optionalType === 'COMPOSE') {
dispatch(cancelReplyCompose());
}
dispatch(closeModal());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot);

View File

@ -1,25 +1,25 @@
import ImmutablePureComponent from 'react-immutable-pure-component';
import { load } from '../actions/status_revision_list';
import StatusRevisionList from '../components/status_revision_list';
import ImmutablePureComponent from 'react-immutable-pure-component'
import { load } from '../actions/status_revision_list'
import StatusRevisionList from '../components/status_revision_list'
class StatusRevisionListContainer extends ImmutablePureComponent {
componentDidMount() {
this.props.load(this.props.id);
}
componentDidMount() {
this.props.load(this.props.id)
}
render() {
return <StatusRevisionList {...this.props} />;
}
render() {
return <StatusRevisionList {...this.props} />
}
}
const mapStateToProps = state => ({
loading: state.getIn(['status_revision_list', 'loading']),
error: state.getIn(['status_revision_list', 'error']),
data: state.getIn(['status_revision_list', 'data']),
});
loading: state.getIn(['status_revision_list', 'loading']),
error: state.getIn(['status_revision_list', 'error']),
data: state.getIn(['status_revision_list', 'data']),
})
const mapDispatchToProps = {
load
};
load
}
export default connect(mapStateToProps, mapDispatchToProps)(StatusRevisionListContainer);
export default connect(mapStateToProps, mapDispatchToProps)(StatusRevisionListContainer)

View File

@ -8,7 +8,7 @@ import { hydrateStore } from '../actions/store';
import initialState from '../initial_state';
import PublicTimeline from '../features/standalone/public_timeline';
import HashtagTimeline from '../features/standalone/hashtag_timeline';
import ModalContainer from './modal_container';
import ModalRoot from '../components/modal/modal_root'
const { localeData, messages } = getLocale();
addLocaleData(localeData);

View File

@ -8,7 +8,6 @@ import Button from '../../../components/button';
import { autoPlayGif, me, isStaff } from '../../../initial_state';
import Avatar from '../../../components/avatar';
import { shortNumberFormat } from '../../../utils/numbers';
import Dropdown from '../../../components/dropdown_menu'
import ProfileInfoPanel from './profile_info_panel/profile_info_panel';
const messages = defineMessages({

View File

@ -1,6 +1,5 @@
import { defineMessages, injectIntl } from 'react-intl';
import { openModal } from '../../../../actions/modal';
import Dropdown from '../../../../components/dropdown_menu'
import { meUsername } from '../../../../initial_state';
const messages = defineMessages({

View File

@ -5,27 +5,30 @@ export default class CharacterCounter extends PureComponent {
static propTypes = {
text: PropTypes.string.isRequired,
max: PropTypes.number.isRequired,
small: PropTypes.bool,
}
render () {
const radius = 12
const { text, max, small } = this.props
const actualRadius = small ? '10' : '16'
const radius = small ? 8 : 12
const circumference = 2 * Math.PI * radius
const diff = length(this.props.text) / this.props.max
const diff = length(text) / max
const dashoffset = circumference * (1 - diff)
return (
<div className={[_s.default, _s.marginRight10PX, _s.justifyContentCenter, _s.alignItemsCenter].join(' ')}>
<svg width="32" height="32" viewBox="0 0 32 32">
<circle fill='none' cx="16" cy="16" r="12" fill="none" stroke="#e6e6e6" strokeWidth="2" />
<svg width={actualRadius * 2} height={actualRadius * 2} viewBox={`0 0 ${actualRadius * 2} ${actualRadius * 2}`}>
<circle fill='none' cx={actualRadius} cy={actualRadius} r={radius} fill="none" stroke="#e6e6e6" strokeWidth="2" />
<circle style={{
// transform: 'rotate(-90deg)',
strokeDashoffset: dashoffset,
strokeDasharray: circumference,
}}
fill='none'
cx="16"
cy="16"
r="12"
cx={actualRadius}
cy={actualRadius}
r={radius}
strokeWidth="2"
strokeLinecap='round'
stroke='#21cf7a'

View File

@ -9,6 +9,7 @@ export default class ComposeExtraButton extends PureComponent {
disabled: PropTypes.bool,
onClick: PropTypes.func,
icon: PropTypes.string,
small: PropTypes.bool,
}
state = {
@ -24,18 +25,26 @@ export default class ComposeExtraButton extends PureComponent {
}
render() {
const { title, disabled, onClick, icon, children } = this.props
const { title, disabled, onClick, icon, children, small } = this.props
const { hovering } = this.state
const containerClasses = cx({
default: 1,
marginRight10PX: !small,
marginRight2PX: small,
})
const btnClasses = cx({
default: 1,
circle: 1,
flexRow: 1,
paddingVertical10PX: 1,
paddingHorizontal10PX: 1,
cursorPointer: 1,
backgroundSubtle: !hovering,
backgroundSubtle2: hovering,
paddingVertical10PX: !small,
paddingHorizontal10PX: !small,
paddingVertical5PX: small,
paddingHorizontal5PX: small,
})
const titleClasses = cx({
@ -49,8 +58,10 @@ export default class ComposeExtraButton extends PureComponent {
displayNone: !hovering,
})
const iconSize = !!small ? '12px' : '18px'
return (
<div className={[_s.default, _s.marginRight10PX].join(' ')}>
<div className={containerClasses}>
<button
className={btnClasses}
title={title}
@ -59,10 +70,13 @@ export default class ComposeExtraButton extends PureComponent {
onMouseEnter={() => this.handleOnMouseEnter()}
onMouseLeave={() => this.handleOnMouseLeave()}
>
<Icon id={icon} width='18px' height='18px' className={_s.fillcolorSecondary} />
<span className={titleClasses}>
{title}
</span>
<Icon id={icon} width={iconSize} height={iconSize} className={_s.fillcolorSecondary} />
{
!small &&
<span className={titleClasses}>
{title}
</span>
}
</button>
{children}
</div>

View File

@ -2,7 +2,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import ImmutablePropTypes from 'react-immutable-proptypes';
import classNames from 'classnames';
import classNames from 'classnames/bind'
import CharacterCounter from '../character_counter';
import UploadForm from '../upload_form';
import ReplyIndicatorContainer from '../../containers/reply_indicator_container';
@ -18,6 +18,7 @@ import SchedulePostDropdown from '../../components/schedule_post_dropdown';
import QuotedStatusPreviewContainer from '../../containers/quoted_status_preview_container';
import Icon from '../../../../components/icon';
import Button from '../../../../components/button';
import Avatar from '../../../../components/avatar'
import { isMobile } from '../../../../utils/is_mobile';
import { countableText } from '../../util/counter';
@ -32,6 +33,8 @@ const messages = defineMessages({
schedulePost: { id: 'compose_form.schedule_post', defaultMessage: 'Schedule Post' }
});
const cx = classNames.bind(_s)
export default
@injectIntl
class ComposeForm extends ImmutablePureComponent {
@ -49,6 +52,8 @@ class ComposeForm extends ImmutablePureComponent {
edit: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired,
suggestions: ImmutablePropTypes.list,
account: ImmutablePropTypes.map.isRequired,
status: ImmutablePropTypes.map,
spoiler: PropTypes.bool,
privacy: PropTypes.string,
spoilerText: PropTypes.string,
@ -164,7 +169,7 @@ class ComposeForm extends ImmutablePureComponent {
document.removeEventListener("click", this.handleClick, false);
}
componentDidUpdate (prevProps) {
componentDidUpdate(prevProps) {
if (!this.autosuggestTextarea) return;
// This statement does several things:
@ -176,13 +181,13 @@ class ComposeForm extends ImmutablePureComponent {
let selectionEnd, selectionStart;
if (this.props.preselectDate !== prevProps.preselectDate) {
selectionEnd = this.props.text.length;
selectionEnd = this.props.text.length;
selectionStart = this.props.text.search(/\s/) + 1;
} else if (typeof this.props.caretPosition === 'number') {
selectionStart = this.props.caretPosition;
selectionEnd = this.props.caretPosition;
selectionEnd = this.props.caretPosition;
} else {
selectionEnd = this.props.text.length;
selectionEnd = this.props.text.length;
selectionStart = selectionEnd;
}
@ -211,30 +216,65 @@ class ComposeForm extends ImmutablePureComponent {
this.props.onPickEmoji(position, data, needsSpace);
}
render () {
const { intl, onPaste, showSearch, anyMedia, shouldCondense, autoFocus, isModalOpen, quoteOfId, edit, scheduledAt } = this.props;
render() {
const {
intl,
account,
onPaste,
showSearch,
anyMedia,
shouldCondense,
autoFocus,
isModalOpen,
quoteOfId,
edit,
scheduledAt
} = this.props
const condensed = shouldCondense && !this.props.text && !this.state.composeFocused;
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) > maxPostCharacterCount || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth)
const composeClassNames = classNames({
'compose-form': true,
'condensed': condensed,
});
const containerClasses = cx({
default: 1,
flexGrow1: 1,
flexRow: shouldCondense,
radiusSmall: shouldCondense,
backgroundSubtle: shouldCondense,
paddingHorizontal5PX: shouldCondense,
})
const actionsContainerClasses = cx({
default: 1,
flexRow: 1,
alignItemsCenter: 1,
marginTop10PX: !shouldCondense,
})
const avatarContainerClasses = cx({
default: 1,
marginRight10PX: 1,
marginTop5PX: shouldCondense,
})
const avatarSize = shouldCondense ? 28 : 46
return (
<div
className={[_s.default, _s.flexGrow1].join(' ')}
ref={this.setForm}
onClick={this.handleClick}
>
{ /* <WarningContainer /> */ }
<div className={[_s.default, _s.flexRow, _s.width100PC].join(' ')}>
<div className={avatarContainerClasses}>
<Avatar account={account} size={avatarSize} />
</div>
<div
className={containerClasses}
ref={this.setForm}
onClick={this.handleClick}
>
{ /* <WarningContainer /> */}
{ /* !shouldCondense && <ReplyIndicatorContainer /> */ }
{ /* !shouldCondense && <ReplyIndicatorContainer /> */}
{ /*
{ /*
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
<AutosuggestTextbox
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
@ -254,63 +294,68 @@ class ComposeForm extends ImmutablePureComponent {
</div>
*/ }
{ /*
{ /*
<div className='emoji-picker-wrapper'>
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
</div> */ }
<AutosuggestTextbox
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={disabled}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onKeyDown={this.handleKeyDown}
onFocus={this.handleComposeFocus}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={shouldAutoFocus}
textarea
>
{ /*
<AutosuggestTextbox
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={disabled}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onKeyDown={this.handleKeyDown}
onFocus={this.handleComposeFocus}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={shouldAutoFocus}
small={shouldCondense}
textarea
>
{ /*
!condensed &&
<div className='compose-form__modifiers'>
<UploadForm />
{!edit && <PollFormContainer />}
</div>
*/ }
</AutosuggestTextbox>
</AutosuggestTextbox>
{ /* quoteOfId && <QuotedStatusPreviewContainer id={quoteOfId} /> */ }
{ /* quoteOfId && <QuotedStatusPreviewContainer id={quoteOfId} /> */}
{
/* !condensed && */
<div className={[_s.default, _s.flexRow, _s.marginTop10PX].join(' ')}>
<div className={actionsContainerClasses}>
<div className={[_s.default, _s.flexRow, _s.marginRightAuto].join(' ')}>
<UploadButton />
<UploadButton small={shouldCondense} />
{
!edit && <PollButton />
!edit && <PollButton small={shouldCondense} />
}
<PrivacyDropdown />
<SpoilerButton />
<SchedulePostDropdown position={isModalOpen ? 'top' : undefined} />
{
!shouldCondense &&
<PrivacyDropdown />
}
<SpoilerButton small={shouldCondense} />
<SchedulePostDropdown small={shouldCondense} position={isModalOpen ? 'top' : undefined} />
</div>
<CharacterCounter max={maxPostCharacterCount} text={text} />
<Button
className={[_s.fontSize15PX, _s.paddingHorizontal15PX].join(' ')}
onClick={this.handleSubmit}
disabled={disabledButton}
>
{intl.formatMessage(scheduledAt ? messages.schedulePost : messages.publish)}
</Button>
<CharacterCounter max={maxPostCharacterCount} text={text} small={shouldCondense} />
{
!shouldCondense &&
<Button
className={[_s.fontSize15PX, _s.paddingHorizontal15PX].join(' ')}
onClick={this.handleSubmit}
disabled={disabledButton}
>
{intl.formatMessage(scheduledAt ? messages.schedulePost : messages.publish)}
</Button>
}
</div>
}
</div>
</div>
);
)
}
}

View File

@ -38,6 +38,7 @@ class PollButton extends PureComponent {
active: PropTypes.bool,
onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
small: PropTypes.bool,
};
handleClick = () => {
@ -45,7 +46,7 @@ class PollButton extends PureComponent {
}
render() {
const { intl, active, unavailable, disabled } = this.props;
const { intl, active, unavailable, disabled, small } = this.props;
if (unavailable) return null;
@ -55,6 +56,7 @@ class PollButton extends PureComponent {
disabled={disabled}
onClick={this.handleClick}
icon='poll'
small={small}
/>
)
}

View File

@ -56,6 +56,7 @@ class SchedulePostDropdown extends PureComponent {
isPro: PropTypes.bool,
onOpenProUpgradeModal: PropTypes.func.isRequired,
position: PropTypes.string,
small: PropTypes.bool,
}
handleToggle = () => {
@ -73,7 +74,7 @@ class SchedulePostDropdown extends PureComponent {
}
render () {
const { intl, date, isPro, position } = this.props
const { intl, date, isPro, position, small } = this.props
const open = !!date
const datePickerDisabled = !isPro
@ -87,6 +88,7 @@ class SchedulePostDropdown extends PureComponent {
icon='calendar'
title={intl.formatMessage(messages.schedule_status)}
onClick={this.handleToggle}
small={small}
/>
</div>
{

View File

@ -28,6 +28,7 @@ class SpoilerButton extends PureComponent {
static propTypes = {
active: PropTypes.bool,
intl: PropTypes.map,
small: PropTypes.bool,
}
handleClick = (e) => {
@ -36,13 +37,14 @@ class SpoilerButton extends PureComponent {
}
render () {
const { active, intl } = this.props
const { active, intl, small } = this.props
return (
<ComposeExtraButton
title={intl.formatMessage(messages.title)}
icon='warning'
onClick={this.handleClick}
small={small}
/>
)
}

View File

@ -39,6 +39,7 @@ class UploadButton extends ImmutablePureComponent {
resetFileKey: PropTypes.number,
acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
intl: PropTypes.object.isRequired,
small: PropTypes.bool,
}
handleChange = (e) => {
@ -56,7 +57,7 @@ class UploadButton extends ImmutablePureComponent {
}
render() {
const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props
const { intl, resetFileKey, unavailable, disabled, acceptContentTypes, small } = this.props
if (unavailable) return null
@ -66,6 +67,7 @@ class UploadButton extends ImmutablePureComponent {
disabled={disabled}
onClick={this.handleClick}
icon='media'
small={small}
>
<label>
<span className={_s.displayNone}>{intl.formatMessage(messages.upload)}</span>

View File

@ -1,4 +1,4 @@
import ComposeForm from '../components/compose_form';
import ComposeForm from '../components/compose_form'
import {
changeCompose,
submitCompose,
@ -9,66 +9,74 @@ import {
insertEmojiCompose,
uploadCompose,
changeScheduledAt,
} from '../../../actions/compose';
} from '../../../actions/compose'
import { me } from '../../../initial_state'
const mapStateToProps = state => ({
edit: state.getIn(['compose', 'id']) !== null,
text: state.getIn(['compose', 'text']),
suggestions: state.getIn(['compose', 'suggestions']),
spoiler: state.getIn(['compose', 'spoiler']),
spoilerText: state.getIn(['compose', 'spoiler_text']),
privacy: state.getIn(['compose', 'privacy']),
focusDate: state.getIn(['compose', 'focusDate']),
caretPosition: state.getIn(['compose', 'caretPosition']),
preselectDate: state.getIn(['compose', 'preselectDate']),
isSubmitting: state.getIn(['compose', 'is_submitting']),
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
isUploading: state.getIn(['compose', 'is_uploading']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
isModalOpen: state.get('modal').modalType === 'COMPOSE',
quoteOfId: state.getIn(['compose', 'quote_of_id']),
scheduledAt: state.getIn(['compose', 'scheduled_at']),
});
const mapStateToProps = (state, { status }) => {
// : todo :
//everything needs to be in relation to if there's a status or not
return {
edit: state.getIn(['compose', 'id']) !== null,
text: state.getIn(['compose', 'text']),
suggestions: state.getIn(['compose', 'suggestions']),
spoiler: state.getIn(['compose', 'spoiler']),
spoilerText: state.getIn(['compose', 'spoiler_text']),
privacy: state.getIn(['compose', 'privacy']),
focusDate: state.getIn(['compose', 'focusDate']),
caretPosition: state.getIn(['compose', 'caretPosition']),
preselectDate: state.getIn(['compose', 'preselectDate']),
isSubmitting: state.getIn(['compose', 'is_submitting']),
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
isUploading: state.getIn(['compose', 'is_uploading']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
isModalOpen: state.get('modal').modalType === 'COMPOSE',
quoteOfId: state.getIn(['compose', 'quote_of_id']),
scheduledAt: state.getIn(['compose', 'scheduled_at']),
account: state.getIn(['accounts', me]),
}
}
const mapDispatchToProps = (dispatch) => ({
onChange (text) {
dispatch(changeCompose(text));
onChange(text) {
dispatch(changeCompose(text))
},
onSubmit (router, group) {
dispatch(submitCompose(router, group));
onSubmit(router, group) {
dispatch(submitCompose(router, group))
},
onClearSuggestions () {
dispatch(clearComposeSuggestions());
onClearSuggestions() {
dispatch(clearComposeSuggestions())
},
onFetchSuggestions (token) {
dispatch(fetchComposeSuggestions(token));
onFetchSuggestions(token) {
dispatch(fetchComposeSuggestions(token))
},
onSuggestionSelected (position, token, suggestion, path) {
dispatch(selectComposeSuggestion(position, token, suggestion, path));
onSuggestionSelected(position, token, suggestion, path) {
dispatch(selectComposeSuggestion(position, token, suggestion, path))
},
onChangeSpoilerText (checked) {
dispatch(changeComposeSpoilerText(checked));
onChangeSpoilerText(checked) {
dispatch(changeComposeSpoilerText(checked))
},
onPaste (files) {
dispatch(uploadCompose(files));
onPaste(files) {
dispatch(uploadCompose(files))
},
onPickEmoji (position, data, needsSpace) {
dispatch(insertEmojiCompose(position, data, needsSpace));
onPickEmoji(position, data, needsSpace) {
dispatch(insertEmojiCompose(position, data, needsSpace))
},
setScheduledAt (date) {
dispatch(changeScheduledAt(date));
setScheduledAt(date) {
dispatch(changeScheduledAt(date))
},
});
})
function mergeProps(stateProps, dispatchProps, ownProps) {
return Object.assign({}, ownProps, {
@ -77,4 +85,4 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
})
}
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(ComposeForm);
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(ComposeForm)

View File

@ -2,7 +2,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { debounce } from 'lodash';
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites';
import { fetchFavoritedStatuses, expandFavoritedStatuses } from '../../actions/favourites';
import { meUsername } from '../../initial_state';
import StatusList from '../../components/status_list';
import ColumnIndicator from '../../components/column_indicator';
@ -29,11 +29,11 @@ class Favourites extends ImmutablePureComponent {
};
componentWillMount () {
this.props.dispatch(fetchFavouritedStatuses());
this.props.dispatch(fetchFavoritedStatuses());
}
handleLoadMore = debounce(() => {
this.props.dispatch(expandFavouritedStatuses());
this.props.dispatch(expandFavoritedStatuses());
}, 300, { leading: true })
render () {

View File

@ -10,7 +10,6 @@ import {
import { FormattedMessage } from 'react-intl';
import AccountContainer from '../../../containers/account_container';
import ScrollableList from '../../../components/scrollable_list';
import Dropdown from '../../../components/dropdown_menu'
const mapStateToProps = (state, { params: { id } }) => ({
group: state.getIn(['groups', id]),

View File

@ -2,7 +2,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { NavLink } from 'react-router-dom';
import Dropdown from '../../../../components/dropdown_menu'
import Button from '../../../../components/button';
const messages = defineMessages({

View File

@ -1,24 +1,27 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { connectGroupStream } from '../../../actions/streaming';
import { expandGroupTimeline } from '../../../actions/timelines';
import StatusListContainer from '../../../containers/status_list_container';
import ColumnSettingsContainer from './containers/column_settings_container';
import Icon from '../../../components/icon';
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { injectIntl, defineMessages } from 'react-intl'
import { Link } from 'react-router-dom'
import classNames from 'classnames'
import { connectGroupStream } from '../../../actions/streaming'
import { expandGroupTimeline } from '../../../actions/timelines'
import StatusListContainer from '../../../containers/status_list_container'
import ColumnSettingsContainer from './containers/column_settings_container'
import Icon from '../../../components/icon'
import ColumnIndicator from '../../../components/column_indicator'
const messages = defineMessages({
tabLatest: { id: 'group.timeline.tab_latest', defaultMessage: 'Latest' },
show: { id: 'group.timeline.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'group.timeline.hide_settings', defaultMessage: 'Hide settings' },
});
tabLatest: { id: 'group.timeline.tab_latest', defaultMessage: 'Latest' },
show: { id: 'group.timeline.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'group.timeline.hide_settings', defaultMessage: 'Hide settings' },
empty: { id: 'empty_column.group', defaultMessage: 'There is nothing in this group yet. When members of this group post new statuses, they will appear here.' },
})
const mapStateToProps = (state, props) => ({
group: state.getIn(['groups', props.params.id]),
relationships: state.getIn(['group_relationships', props.params.id]),
hasUnread: state.getIn(['timelines', `group:${props.params.id}`, 'unread']) > 0,
});
group: state.getIn(['groups', props.params.id]),
relationships: state.getIn(['group_relationships', props.params.id]),
hasUnread: state.getIn(['timelines', `group:${props.params.id}`, 'unread']) > 0,
})
export default
@connect(mapStateToProps)
@ -26,105 +29,71 @@ export default
class GroupTimeline extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
router: PropTypes.object,
}
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
hasUnread: PropTypes.bool,
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
relationships: ImmutablePropTypes.map,
intl: PropTypes.object.isRequired,
};
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
hasUnread: PropTypes.bool,
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
relationships: ImmutablePropTypes.map,
intl: PropTypes.object.isRequired,
}
state = {
collapsed: true,
}
componentDidMount () {
const { dispatch } = this.props;
const { id } = this.props.params;
componentDidMount() {
const { dispatch } = this.props
const { id } = this.props.params
dispatch(expandGroupTimeline(id));
dispatch(expandGroupTimeline(id))
this.disconnect = dispatch(connectGroupStream(id));
this.disconnect = dispatch(connectGroupStream(id))
}
componentWillUnmount () {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
componentWillUnmount() {
if (this.disconnect) {
this.disconnect()
this.disconnect = null
}
}
handleLoadMore = maxId => {
const { id } = this.props.params;
this.props.dispatch(expandGroupTimeline(id, { maxId }));
const { id } = this.props.params
this.props.dispatch(expandGroupTimeline(id, { maxId }))
}
handleToggleClick = (e) => {
e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed });
}
e.stopPropagation()
this.setState({ collapsed: !this.state.collapsed })
}
render () {
const { columnId, group, relationships, account, intl } = this.props;
const { collapsed } = this.state;
const { id } = this.props.params;
render() {
const { columnId, group, relationships, account, intl } = this.props
const { collapsed } = this.state
const { id } = this.props.params
if (typeof group === 'undefined' || !relationships) {
return (<ColumnIndicator type='loading' />);
} else if (group === false) {
return (<ColumnIndicator type='missing' />);
}
return (
<div>
{
relationships.get('member') &&
<TimelineComposeBlock size={46} group={group} shouldCondense={true} autoFocus={false} />
}
<div className='group__feed'>
<div className="column-header__wrapper">
<h1 className="column-header">
<Link to={`/groups/${id}`} className={classNames('btn grouped active')}>
{intl.formatMessage(messages.tabLatest)}
</Link>
<div className='column-header__buttons'>
<button
className={classNames('column-header__button', { 'active': !collapsed })}
title={intl.formatMessage(collapsed ? messages.show : messages.hide)}
aria-label={intl.formatMessage(collapsed ? messages.show : messages.hide)}
aria-pressed={collapsed ? 'false' : 'true'}
onClick={this.handleToggleClick}
><Icon id='sliders' /></button>
</div>
</h1>
{!collapsed && <div className='column-header__collapsible'>
<div className='column-header__collapsible-inner'>
<div className='column-header__collapsible__extra'>
<ColumnSettingsContainer />
</div>
</div>
</div>}
</div>
<StatusListContainer
alwaysPrepend
scrollKey={`group_timeline-${columnId}`}
timelineId={`group:${id}`}
onLoadMore={this.handleLoadMore}
group={group}
withGroupAdmin={relationships && relationships.get('admin')}
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group post new statuses, they will appear here.' />}
/>
</div>
</div>
);
if (typeof group === 'undefined' || !relationships) {
return (<ColumnIndicator type='loading' />)
} else if (group === false) {
return (<ColumnIndicator type='missing' />)
}
return (
<StatusListContainer
alwaysPrepend
scrollKey={`group_timeline-${columnId}`}
timelineId={`group:${id}`}
onLoadMore={this.handleLoadMore}
group={group}
withGroupAdmin={relationships && relationships.get('admin')}
emptyMessage={intl.formatMessage(messages.empty)}
/>
)
}
}

View File

@ -1 +0,0 @@
export { default } from './pinned_statuses';

View File

@ -1,49 +0,0 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
import { meUsername } from '../../initial_state';
import StatusList from '../../components/status_list/status_list';
import ColumnIndicator from '../../components/column_indicator';
const mapStateToProps = (state, { params: { username } }) => {
return {
isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()),
statusIds: state.getIn(['status_lists', 'pins', 'items']),
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
};
};;
export default
@connect(mapStateToProps)
class PinnedStatuses extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
hasMore: PropTypes.bool.isRequired,
isMyAccount: PropTypes.bool.isRequired,
};
componentWillMount () {
this.props.dispatch(fetchPinnedStatuses());
}
render () {
const { statusIds, hasMore, isMyAccount } = this.props;
if (!isMyAccount) {
return <ColumnIndicator type='missing' />
}
return (
<StatusList
statusIds={statusIds}
scrollKey='pinned_statuses'
hasMore={hasMore}
emptyMessage={<FormattedMessage id='pinned_statuses.none' defaultMessage='No pins to show.' />}
/>
);
}
}

View File

@ -1,7 +1,7 @@
import ComposeFormContainer from '../../compose/containers/compose_form_container';
import NotificationsContainer from '../../../containers/notifications_container';
import LoadingBarContainer from '../../../containers/loading_bar_container';
import ModalContainer from '../../../containers/modal_container';
import ModalRoot from '../../../components/modal/modal_root'
export default class Compose extends PureComponent {
@ -10,7 +10,7 @@ export default class Compose extends PureComponent {
<div>
<ComposeFormContainer />
<NotificationsContainer />
<ModalContainer />
<ModalRoot />
<LoadingBarContainer className='loading-bar' />
</div>
);

View File

@ -3,7 +3,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { openModal } from '../../../../actions/modal';
import { me, isStaff } from '../../../../initial_state';
import Dropdown from '../../../../components/dropdown_menu';
import IconButton from '../../../../components/icon_button';
const messages = defineMessages({

View File

@ -15,7 +15,8 @@ import { clearHeight } from '../../actions/height_cache'
import { openModal } from '../../actions/modal'
import WrappedRoute from './util/wrapped_route'
import NotificationsContainer from '../../containers/notifications_container'
import ModalContainer from '../../containers/modal_container'
import ModalRoot from '../../components/modal/modal_root'
import PopoverRoot from '../../components/popover/popover_root'
import UploadArea from '../../components/upload_area'
// import TrendsPanel from './components/trends_panel'
// import { WhoToFollowPanel } from '../../components/panel'
@ -80,7 +81,6 @@ const mapStateToProps = state => ({
isComposing: state.getIn(['compose', 'is_composing']),
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
})
const keyMap = {
@ -243,7 +243,6 @@ class UI extends PureComponent {
hasMediaAttachments: PropTypes.bool,
location: PropTypes.object,
intl: PropTypes.object.isRequired,
dropdownMenuIsOpen: PropTypes.bool,
}
state = {
@ -504,7 +503,7 @@ class UI extends PureComponent {
render() {
const { draggingOver } = this.state
const { children, location, dropdownMenuIsOpen } = this.props
const { children, location } = this.props
const handlers = me ? {
help: this.handleHotkeyToggleHelp,
@ -532,12 +531,7 @@ class UI extends PureComponent {
attach={window}
focused
>
<div
ref={this.setRef}
style={{
pointerEvents: dropdownMenuIsOpen ? 'none' : null
}}
>
<div ref={this.setRef}>
<SwitchingArea
location={location}
onLayoutChange={this.handleLayoutChange}
@ -546,7 +540,8 @@ class UI extends PureComponent {
</SwitchingArea>
<NotificationsContainer />
<ModalContainer />
<ModalRoot />
<PopoverRoot />
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
</div>
</HotKeys>

View File

@ -58,10 +58,6 @@ export function Status() {
return import(/* webpackChunkName: "features/status" */'../../status')
}
export function PinnedStatuses() {
return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses')
}
export function AccountTimeline() {
return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline')
}
@ -90,7 +86,7 @@ export function GenericNotFound() {
return import(/* webpackChunkName: "features/generic_not_found" */'../../generic_not_found')
}
export function FavouritedStatuses() {
export function FavoritedStatuses() {
return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses')
}

View File

@ -0,0 +1,106 @@
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import Sticky from 'react-stickynode'
import Sidebar from '../components/sidebar'
import Search from '../components/search'
import Image from '../components/image'
import Text from '../components/text'
import Button from '../components/button'
import DisplayName from '../components/display_name'
import TabBar from '../components/tab_bar'
import ColumnHeader from '../components/column_header'
export default class GroupLayout extends ImmutablePureComponent {
static propTypes = {
actions: PropTypes.array,
tabs: PropTypes.array,
group: ImmutablePropTypes.map,
layout: PropTypes.object,
title: PropTypes.string,
showBackBtn: PropTypes.bool,
}
render() {
const { group, children, layout, title, showBackBtn, actions, tabs } = this.props
// const tabs = !account ? null : [
// {
// to: `/${account.get('acct')}`,
// title: 'Timeline',
// },
// {
// to: `/${account.get('acct')}/comments`,
// title: 'Comments',
// },
// {
// to: `/${account.get('acct')}/photos`,
// title: 'Photos',
// },
// {
// to: `/${account.get('acct')}/videos`,
// title: 'Videos',
// },
// {
// to: '',
// title: 'More',
// },
// ]
return (
<div className={[_s.default, _s.flexRow, _s.width100PC, _s.heightMin100VH, _s.backgroundcolorSecondary3].join(' ')}>
<Sidebar />
<main role='main' className={[_s.default, _s.flexShrink1, _s.flexGrow1, _s.borderColorSecondary2, _s.borderLeft1PX].join(' ')}>
<div className={[_s.default, _s.height53PX, _s.borderBottom1PX, _s.borderColorSecondary2, _s.backgroundcolorSecondary3, _s.z3, _s.top0, _s.positionFixed].join(' ')}>
<div className={[_s.default, _s.height53PX, _s.paddingLeft15PX, _s.width1015PX, _s.flexRow, _s.justifyContentSpaceBetween].join(' ')}>
<div className={[_s.default, _s.width645PX].join(' ')}>
<ColumnHeader
title={title}
showBackBtn={showBackBtn}
actions={actions}
tabs={tabs}
/>
</div>
<div className={[_s.default, _s.width340PX].join(' ')}>
<Search />
</div>
</div>
</div>
<div className={[_s.default, _s.height53PX].join(' ')}></div>
<div className={[_s.default, _s.width1015PX, _s.paddingLeft15PX, _s.paddingVertical15PX].join(' ')}>
<div className={[_s.default, _s.z1, _s.width100PC, _s.marginBottom15PX].join(' ')}>
<div className={[_s.default, _s.height350PX, _s.width100PC, _s.radiusSmall, _s.overflowHidden].join(' ')}>
<Image className={_s.height350PX} src='https://gab.com/media/user/bz-5cf53d08403d4.jpeg' />
</div>
</div>
<div className={[_s.default, _s.flexRow, _s.width100PC, _s.justifyContentSpaceBetween].join(' ')}>
<div className={[_s.default, _s.width645PX, _s.z1].join(' ')}>
<div className={_s.default}>
{children}``
</div>
</div>
<div className={[_s.default, _s.width340PX].join(' ')}>
<Sticky top={73} enabled>
<div className={[_s.default, _s.width340PX].join(' ')}>
{layout}
</div>
</Sticky>
</div>
</div>
</div>
</main>
</div>
)
}
}

View File

@ -1,16 +1,46 @@
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import Sticky from 'react-stickynode'
import Sidebar from '../components/sidebar'
import Image from '../components/image'
import Text from '../components/text'
import Button from '../components/button'
import DisplayName from '../components/display_name'
import TabBar from '../components/tab_bar'
export default class ProfileLayout extends PureComponent {
export default class ProfileLayout extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
layout: PropTypes.object,
title: PropTypes.string,
showBackBtn: PropTypes.bool,
}
render() {
const { children, layout } = this.props
const { account, children, layout } = this.props
const tabs = !account ? null : [
{
to: `/${account.get('acct')}`,
title: 'Timeline',
},
{
to: `/${account.get('acct')}/comments`,
title: 'Comments',
},
{
to: `/${account.get('acct')}/photos`,
title: 'Photos',
},
{
to: `/${account.get('acct')}/videos`,
title: 'Videos',
},
{
to: '',
title: 'More',
},
]
return (
<div className={[_s.default, _s.flexRow, _s.width100PC, _s.heightMin100VH, _s.backgroundcolorSecondary3].join(' ')}>
@ -21,14 +51,67 @@ export default class ProfileLayout extends PureComponent {
<div className={[_s.default, _s.width1015PX, _s.flexRow, _s.justifyContentSpaceBetween, _s.paddingLeft15PX, _s.paddingVertical15PX].join(' ')}>
<div className={[_s.default, _s.z1, _s.width100PC].join(' ')}>
<div className={[_s.default, _s.height350PX, _s.width100PC].join(' ')}>
<div className={[_s.default, _s.height350PX, _s.width100PC, _s.radiusSmall, _s.overflowHidden].join(' ')}>
<Image className={_s.height350PX} src='https://gab.com/media/user/bz-5cf53d08403d4.jpeg' />
</div>
<div className={[_s.default, _s.backgroundColorPrimary].join(' ')}>
<div className={[_s.default, _s.borderBottom1PX, _s.borderColorSecondary, _s.width100PC].join(' ')}>
<div className={[_s.default, _s.flexRow, _s.paddingHorizontal15PX].join(' ')}>
<Image
className={[_s.circle, _s.marginTopNeg75PX, _s.borderColorWhite, _s.border2PX].join(' ')}
height='150px'
width='150px'
src='http://localhost:3000/system/accounts/avatars/000/000/001/original/260e8c96c97834da.jpeg?1562898139'
/>
<div className={[_s.default, _s.paddingHorizontal15PX, _s.paddingVertical10PX].join(' ')}>
<DisplayName account={account} multiline large />
</div>
</div>
<div className={[_s.default, _s.flexRow, _s.borderBottom1PX, _s.borderColorSecondary, _s.marginTop5PX, _s.height53PX].join(' ')}>
<div className={[_s.default].join(' ')}>
<TabBar tabs={tabs} large />
</div>
<div className={[_s.default, _s.flexRow, _s.marginLeftAuto, _s.paddingVertical5PX].join(' ')}>
<Button
outline
icon='ellipsis'
iconWidth='18px'
iconHeight='18px'
iconClassName={_s.fillColorBrand}
color='brand'
backgroundColor='none'
className={[_s.justifyContentCenter, _s.alignItemsCenter, _s.marginRight10PX, _s.paddingHorizontal10PX].join(' ')}
/>
<Button
outline
icon='chat'
iconWidth='18px'
iconHeight='18px'
iconClassName={_s.fillColorBrand}
color='brand'
backgroundColor='none'
className={[_s.justifyContentCenter, _s.alignItemsCenter, _s.marginRight10PX, _s.paddingHorizontal10PX].join(' ')}
/>
<Button
className={[_s.justifyContentCenter, _s.alignItemsCenter].join(' ')}
>
<span className={[_s.paddingHorizontal15PX].join(' ')}>
<Text
color='white'
weight='bold'
size='medium'
className={[_s.paddingHorizontal15PX].join(' ')}
>
Follow
</Text>
</span>
</Button>
</div>
</div>
</div>
<div className={[_s.default, _s.width1015PX, _s.flexRow, _s.justifyContentSpaceBetween, _s.paddingLeft15PX, _s.paddingVertical15PX].join(' ')}>
<div className={[_s.default, _s.width1015PX, _s.flexRow, _s.justifyContentSpaceBetween, _s.paddingRight15PX, _s.paddingVertical15PX].join(' ')}>
<div className={[_s.default, _s.width645PX, _s.z1].join(' ')}>
<div className={_s.default}>
{children}
@ -36,7 +119,7 @@ export default class ProfileLayout extends PureComponent {
</div>
<div className={[_s.default, _s.width340PX].join(' ')}>
<Sticky top={73} enabled>
<Sticky top={15} enabled>
<div className={[_s.default, _s.width340PX].join(' ')}>
{layout}
</div>

View File

@ -1,62 +1,90 @@
import { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { fetchGroup } from '../actions/groups';
import HeaderContainer from '../features/groups/timeline/containers/header_container';
import GroupPanel from '../features/groups/timeline/components/panel';
// import GroupSidebarPanel from '../features/groups/sidebar_panel';
import DefaultLayout from '../layouts/default_layout';
import { WhoToFollowPanel } from '../components/panel';
import LinkFooter from '../components/link_footer';
import { Fragment } from 'react'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { fetchGroup } from '../actions/groups'
import GroupInfoPanel from '../components/panel/group_info_panel'
import GroupLayout from '../layouts/group_layout'
import WhoToFollowPanel from '../components/panel/who_to_follow_panel'
import LinkFooter from '../components/link_footer'
import TimelineComposeBlock from '../components/timeline_compose_block'
import Divider from '../components/divider'
const mapStateToProps = (state, { params: { id } }) => ({
group: state.getIn(['groups', id]),
relationships: state.getIn(['group_relationships', id]),
});
})
export default
@connect(mapStateToProps)
class GroupPage extends ImmutablePureComponent {
static propTypes = {
group: ImmutablePropTypes.map,
static propTypes = {
group: ImmutablePropTypes.map,
relationships: ImmutablePropTypes.map,
dispatch: PropTypes.func.isRequired,
};
componentWillMount() {
const { params: { id }, dispatch } = this.props;
dispatch(fetchGroup(id));
}
render () {
const { children, group, relationships } = this.props;
const top = group ? <HeaderContainer groupId={group.get('id')} /> : null;
componentWillMount() {
const { params: { id }, dispatch } = this.props
dispatch(fetchGroup(id))
}
render() {
const { children, group, relationships } = this.props
// <div className="column-header__wrapper">
// <h1 className="column-header">
// <Link to={`/groups/${id}`} className={classNames('btn grouped active')}>
// {intl.formatMessage(messages.tabLatest)}
// </Link>
// <div className='column-header__buttons'>
// <button
// className={classNames('column-header__button', { 'active': !collapsed })}
// title={intl.formatMessage(collapsed ? messages.show : messages.hide)}
// aria-label={intl.formatMessage(collapsed ? messages.show : messages.hide)}
// aria-pressed={collapsed ? 'false' : 'true'}
// onClick={this.handleToggleClick}
// ><Icon id='sliders' /></button>
// </div>
// </h1>
// {!collapsed && <div className='column-header__collapsible'>
// <div className='column-header__collapsible-inner'>
// <div className='column-header__collapsible__extra'>
// <ColumnSettingsContainer />
// </div>
// </div>
// </div>}
// </div>
return (
<DefaultLayout
layout={{
TOP: top,
RIGHT: (
<Fragment>
<WhoToFollowPanel />
</Fragment>
),
LEFT: (
<Fragment>
{group && relationships &&
<GroupPanel
group={group}
relationships={relationships}
/>}
<LinkFooter />
</Fragment>
)
}}
<GroupLayout
title={'group name'}
actions={[
{
icon: 'ellipsis',
onClick: null,
},
]}
layout={(
<Fragment>
<GroupInfoPanel />
<WhoToFollowPanel />
<LinkFooter />
</Fragment>
)}
showBackBtn
>
{
!!relationships && relationships.get('member') &&
<Fragment>
<TimelineComposeBlock size={46} group={group} autoFocus />
<Divider />
</Fragment>
}
{children}
</DefaultLayout>
</GroupLayout>
)
}
}

View File

@ -40,7 +40,7 @@ export default class HomePage extends PureComponent {
</Fragment>
)}
>
<TimelineComposeBlock autoFocus={false} shouldCondense />
<TimelineComposeBlock autoFocus={false} />
<Divider />
{children}
</DefaultLayout>

View File

@ -1,7 +1,10 @@
import { Fragment } from 'react'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { fetchAccountByUsername } from '../actions/accounts'
import { makeGetAccount } from '../selectors'
import LinkFooter from '../components/link_footer'
import ProfileStatsPanel from '../components/panel/profile_stats_panel'
import ProfileInfoPanel from '../components/panel/profile_info_panel'
import MediaGalleryPanel from '../components/panel/media_gallery_panel'
import ProfileLayout from '../layouts/profile_layout'
@ -10,24 +13,15 @@ const mapStateToProps = (state, { params: { username }, withReplies = false }) =
const accounts = state.getIn(['accounts'])
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() == username.toLowerCase())
let accountId = -1
let account = null
let accountUsername = username
if (accountFetchError) {
accountId = null
}
else {
if (!accountFetchError) {
account = accounts.find(acct => username.toLowerCase() == acct.getIn(['acct'], '').toLowerCase())
accountId = account ? account.getIn(['id'], null) : -1
accountUsername = account ? account.getIn(['acct'], '') : ''
}
//Children components fetch information
const getAccount = makeGetAccount()
return {
account,
accountId,
accountUsername,
account: account ? getAccount(state, account.get('id')) : null,
}
}
@ -35,30 +29,33 @@ export default
@connect(mapStateToProps)
class ProfilePage extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
account: ImmutablePropTypes.map,
accountUsername: PropTypes.string.isRequired,
accountId: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
dispatch: PropTypes.func.isRequired,
children: PropTypes.node,
}
componentWillMount() {
const { dispatch, params: { username } } = this.props
dispatch(fetchAccountByUsername(username))
}
render() {
const { accountId, account, accountUsername } = this.props
const { account } = this.props
return (
<ProfileLayout
account={account}
layout={(
<Fragment>
<ProfileInfoPanel />
<MediaGalleryPanel />
<ProfileStatsPanel account={account} />
<ProfileInfoPanel account={account} />
<MediaGalleryPanel account={account} />
<LinkFooter />
</Fragment>
)}
>
{ /* this.props.children */ }
{ this.props.children }
</ProfileLayout>
)
}

View File

@ -1,18 +0,0 @@
import Immutable from 'immutable';
import {
DROPDOWN_MENU_OPEN,
DROPDOWN_MENU_CLOSE,
} from '../actions/dropdown_menu';
const initialState = Immutable.Map({ openId: null, placement: null, keyboard: false });
export default function dropdownMenu(state = initialState, action) {
switch (action.type) {
case DROPDOWN_MENU_OPEN:
return state.merge({ openId: action.id, placement: action.placement, keyboard: action.keyboard });
case DROPDOWN_MENU_CLOSE:
return state.get('openId') === action.id ? state.set('openId', null) : state;
default:
return state;
}
}

View File

@ -1,5 +1,5 @@
import { combineReducers } from 'redux-immutable';
import dropdown_menu from './dropdown_menu';
import popover from './popover';
import timelines from './timelines';
import meta from './meta';
import alerts from './alerts';
@ -40,7 +40,7 @@ import sidebar from './sidebar';
import status_revision_list from './status_revision_list';
const reducers = {
dropdown_menu,
popover,
timelines,
meta,
alerts,

View File

@ -0,0 +1,28 @@
import Immutable from 'immutable'
import {
POPOVER_OPEN,
POPOVER_CLOSE,
} from '../actions/popover'
const initialState = Immutable.Map({
popoverType: null,
placement: null,
keyboard: false
})
export default function popoverMenu(state = initialState, action) {
switch (action.type) {
case POPOVER_OPEN:
console.log("POPOVER_OPEN:", action)
return {
popoverType: action.popoverType,
placement: action.placement,
keyboard: action.keyboard
}
case POPOVER_CLOSE:
console.log("POPOVER_CLOSE:", action)
return initialState;
default:
return state
}
}

View File

@ -6,9 +6,6 @@ import {
FAVOURITED_STATUSES_EXPAND_SUCCESS,
FAVOURITED_STATUSES_EXPAND_FAIL,
} from '../actions/favourites';
import {
PINNED_STATUSES_FETCH_SUCCESS,
} from '../actions/pin_statuses';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import {
FAVOURITE_SUCCESS,
@ -75,8 +72,6 @@ export default function statusLists(state = initialState, action) {
return prependOneToList(state, 'favourites', action.status);
case UNFAVOURITE_SUCCESS:
return removeOneFromList(state, 'favourites', action.status);
case PINNED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, 'pins', action.statuses, action.next);
case PIN_SUCCESS:
return prependOneToList(state, 'pins', action.status);
case UNPIN_SUCCESS:

View File

@ -28,6 +28,20 @@ export function breakpointExtraSmall(width) {
return width < BREAKPOINT_EXTRA_SMALL
}
export function getScreenBreakpoint(width) {
if (width > BREAKPOINT_EXTRA_LARGE) {
return 'BREAKPOINT_EXTRA_LARGE'
} else if (width > BREAKPOINT_MEDIUM && width < BREAKPOINT_LARGE) {
return 'BREAKPOINT_LARGE'
} else if (width > BREAKPOINT_SMALL && width < BREAKPOINT_MEDIUM) {
return 'BREAKPOINT_MEDIUM'
} else if (width > BREAKPOINT_EXTRA_SMALL && width < BREAKPOINT_SMALL) {
return 'BREAKPOINT_SMALL'
} else {
return 'BREAKPOINT_EXTRA_SMALL'
}
}
//
const LAYOUT_BREAKPOINT = 630

View File

@ -1,4 +1,5 @@
html, body {
html,
body {
height: 100%;
margin: 0;
padding: 0;
@ -17,6 +18,22 @@ body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif;
}
.statusContent a {
color: #21cf7a;
}
.dangerousContent * {
margin-top: 0;
margin-bottom: 0;
font-size: 14px;
overflow-wrap: break-word;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif;
}
.dangerousContent a {
color: #21cf7a;
}
.statusCardVideo iframe {
height: 100% !important;
width: 100% !important;
@ -68,17 +85,41 @@ body {
flex-shrink: 1;
}
.flexGrow1 { flex-grow: 1; }
.flexShrink1 { flex-shrink: 1; }
.flexRow { flex-direction: row; }
.flexWrap { flex-wrap: wrap; }
.flexGrow1 {
flex-grow: 1;
}
.alignItemsEnd { align-items: flex-end; }
.alignItemsStart { align-items: flex-start; }
.alignItemsCenter { align-items: center; }
.flexShrink1 {
flex-shrink: 1;
}
.justifyContentSpaceBetween { justify-content: space-between; }
.justifyContentCenter { justify-content: center; }
.flexRow {
flex-direction: row;
}
.flexWrap {
flex-wrap: wrap;
}
.alignItemsEnd {
align-items: flex-end;
}
.alignItemsStart {
align-items: flex-start;
}
.alignItemsCenter {
align-items: center;
}
.justifyContentSpaceBetween {
justify-content: space-between;
}
.justifyContentCenter {
justify-content: center;
}
.overflowHidden {
overflow-x: hidden;
@ -93,207 +134,624 @@ body {
white-space: nowrap;
}
.whiteSpaceNoWrap { white-space: nowrap; }
.whiteSpaceNoWrap {
white-space: nowrap;
}
.outlineNone { outline: none; }
.outlineFocusBrand:focus { outline: 2px solid #21cf7a; }
.resizeNone { resize: none; }
.outlineNone {
outline: none;
}
.circle { border-radius: 9999px; }
.radiusSmall { border-radius: 8px; }
.outlineFocusBrand:focus {
outline: 2px solid #21cf7a;
}
.borderColorSecondary2 { border-color: #e5e9ed; }
.borderColorSecondary { border-color: #ECECED; }
.borderColorWhite { border-color: #fff; }
.borderColorBrand { border-color: #21cf7a; }
.borderColorTransparent { border-color: transparent; }
.borderRight1PX { border-right-width: 1px; }
.borderBottom1PX { border-bottom-width: 1px; }
.borderLeft1PX { border-left-width: 1px; }
.borderTop1PX { border-top-width: 1px; }
.border1PX { border-width: 1px; }
.border2PX { border-width: 2px; }
.borderBottom2PX { border-bottom-width: 2px; }
.resizeNone {
resize: none;
}
.borderDashed { border-style: dashed; }
.circle {
border-radius: 9999px;
}
.marginAuto { margin: auto; }
.radiusSmall {
border-radius: 8px;
}
.displayNone { display: none; }
.displayBlock { display: block; }
.displayInline { display: inline; }
.displayFlex { display: flex !important; }
.displayInlineBlock { display: inline-block; }
.borderColorSecondary2 {
border-color: #e5e9ed;
}
.cursorPointer { cursor: pointer }
.borderColorSecondary {
border-color: #ECECED;
}
.pointerEventsAuto > * { pointer-events: auto;}
.pointerEventsNone { pointer-events: none !important; }
.borderColorWhite {
border-color: #fff;
}
.backgroundTransparent { background-color: transparent; }
.backgroundPanel { background-color: #aaa; }
.backgroundSubtle { background-color: #F5F8FA; }
.backgroundSubtle_onHover:hover { background-color: #F5F8FA; }
.backgroundSubtle2 { background-color: #e8ecef; }
.backgroundSubtle2Dark_onHover:hover { background-color: #d9e0e5; }
.backgroundcolorSecondary3 { background-color: #F6F6F9; }
.backgroundColorPrimary { background-color: #fff; }
.backgroundColorPrimaryOpaque { background-color: rgba(255,255,255,0.8); }
.backgroundColorBrandLightOpaque { background-color: rgba(54, 233, 145, 0.1); }
.backgroundColorOpaque { background-color: rgba(0,0,0, 0.4); }
.backgroundColorBrandLight { background-color: #36e991; }
.backgroundColorBrand { background-color: #21cf7a; }
.backgroundColorBrand_onHover:hover { background-color: #21cf7a; }
.backgroundColorBrandDark { background-color: #38A16B; }
.backgroundColorBrandDark_onHover:hover { background-color: #38A16B; }
.colorPrimary { color: #000; }
.colorWhite { color: #fff; }
.colorWhite_onHover:hover { color: #fff; }
.colorSecondary { color: #4B4F55; }
.colorBrand { color: #21cf7a }
.fillColorBlack { fill: #000; }
.fillColorWhite { fill: #fff; }
.fillColorBrand { fill: #21cf7a; }
.fillcolorSecondary { fill: #666; }
.borderColorBrand {
border-color: #21cf7a;
}
.bottom0 { bottom: 0; }
.bottomAuto { bottom: auto; }
.left0 { left: 0px; }
.right0 { right: 0px; }
.rightAuto { right: auto; }
.top0 { top: 0px; }
.borderColorTransparent {
border-color: transparent;
}
.lineHeight125 { line-height: 1.25em; }
.lineHeight15 { line-height: 1.5em; }
.lineHeight2 { line-height: 2em; }
.borderRight1PX {
border-right-width: 1px;
}
.positionFixed { position: fixed; }
.positionSticky { position: sticky; }
.positionRelative { position: relative; }
.positionAbsolute { position: absolute; }
.borderBottom1PX {
border-bottom-width: 1px;
}
.noSelect { user-select: none; }
.borderLeft1PX {
border-left-width: 1px;
}
.heightMin100VH { min-height: 100vh; }
.height100VH { height: 100vh; }
.height100PC { height: 100%; }
.height22PX { height: 22px; }
.height50PX { height: 50px; }
.height53PX { height: 53px; }
.height72PX { height: 72px; }
.height122PX { height: 122px; }
.height260PX { height: 260px; }
.height350PX { height: 350px; }
.borderTop1PX {
border-top-width: 1px;
}
.width1015PX { width: 1015px; }
.width645PX { width: 645px; }
.width400PX { width: 400px; }
.width340PX { width: 340px; }
.width240PX { width: 240px; }
.width100PC { width: 100%; }
.width72PX { width: 72px; }
.width50PC { width: 50%; }
.maxWidth100PC { max-width: 100%; }
.border1PX {
border-width: 1px;
}
.border2PX {
border-width: 2px;
}
.borderBottom2PX {
border-bottom-width: 2px;
}
.borderDashed {
border-style: dashed;
}
.marginAuto {
margin: auto;
}
.displayNone {
display: none;
}
.displayBlock {
display: block;
}
.displayInline {
display: inline;
}
.displayFlex {
display: flex !important;
}
.displayInlineBlock {
display: inline-block;
}
.cursorPointer {
cursor: pointer
}
.pointerEventsAuto>* {
pointer-events: auto;
}
.pointerEventsNone {
pointer-events: none !important;
}
.backgroundTransparent {
background-color: transparent;
}
.backgroundPanel {
background-color: #aaa;
}
.backgroundSubtle {
background-color: #F5F8FA;
}
.backgroundSubtle_onHover:hover {
background-color: #F5F8FA;
}
.backgroundSubtle2 {
background-color: #e8ecef;
}
.backgroundSubtle2Dark_onHover:hover {
background-color: #d9e0e5;
}
.backgroundcolorSecondary3 {
background-color: #F6F6F9;
}
.backgroundColorPrimary {
background-color: #fff;
}
.backgroundColorPrimaryOpaque {
background-color: rgba(255, 255, 255, 0.8);
}
.backgroundColorBrandLightOpaque {
background-color: rgba(54, 233, 145, 0.1);
}
.backgroundColorOpaque {
background-color: rgba(0, 0, 0, 0.4);
}
.backgroundColorBrandLight {
background-color: #36e991;
}
.backgroundColorBrand {
background-color: #21cf7a;
}
.backgroundColorBrand_onHover:hover {
background-color: #21cf7a;
}
.backgroundColorBrandDark {
background-color: #38A16B;
}
.backgroundColorBrandDark_onHover:hover {
background-color: #38A16B;
}
.colorPrimary {
color: #000;
}
.colorWhite {
color: #fff;
}
.colorWhite_onHover:hover {
color: #fff;
}
.colorSecondary {
color: #4B4F55;
}
.colorBrand {
color: #21cf7a
}
.fillColorBlack {
fill: #000;
}
.fillColorWhite {
fill: #fff;
}
.fillColorBrand {
fill: #21cf7a;
}
.fillcolorSecondary {
fill: #666;
}
.bottom0 {
bottom: 0;
}
.bottomAuto {
bottom: auto;
}
.left0 {
left: 0px;
}
.right0 {
right: 0px;
}
.rightAuto {
right: auto;
}
.top0 {
top: 0px;
}
.lineHeight125 {
line-height: 1.25em;
}
.lineHeight15 {
line-height: 1.5em;
}
.lineHeight2 {
line-height: 2em;
}
.positionFixed {
position: fixed;
}
.positionSticky {
position: sticky;
}
.positionRelative {
position: relative;
}
.positionAbsolute {
position: absolute;
}
.noSelect {
user-select: none;
}
.heightMin100VH {
min-height: 100vh;
}
.height100VH {
height: 100vh;
}
.height100PC {
height: 100%;
}
.height22PX {
height: 22px;
}
.height50PX {
height: 50px;
}
.height53PX {
height: 53px;
}
.height72PX {
height: 72px;
}
.height122PX {
height: 122px;
}
.height260PX {
height: 260px;
}
.height350PX {
height: 350px;
}
.width1015PX {
width: 1015px;
}
.width645PX {
width: 645px;
}
.width400PX {
width: 400px;
}
.width340PX {
width: 340px;
}
.width240PX {
width: 240px;
}
.width100PC {
width: 100%;
}
.width72PX {
width: 72px;
}
.width50PC {
width: 50%;
}
.maxWidth100PC {
max-width: 100%;
}
@media (min-width: 1480px) {
.width1015PX { width: 1080px; }
.width645PX { width: 700px; }
.width340PX { width: 350px; }
.width240PX { width: 250px; }
.width1015PX {
width: 1080px;
}
.width645PX {
width: 700px;
}
.width340PX {
width: 350px;
}
.width240PX {
width: 250px;
}
}
@media (min-width: 1160px) and (max-width: 1280px) {
.width1015PX { width: 910px; }
.width645PX { width: 580px; }
.width340PX { width: 300px; }
.width240PX { width: 230px; }
.width1015PX {
width: 910px;
}
.width645PX {
width: 580px;
}
.width340PX {
width: 300px;
}
.width240PX {
width: 230px;
}
}
@media (min-width: 1080px) and (max-width: 1160px) {
.width1015PX { width: 850px; }
.width645PX { width: 525px; }
.width340PX { width: 300px; }
.width240PX { width: 210px; }
.width1015PX {
width: 850px;
}
.width645PX {
width: 525px;
}
.width340PX {
width: 300px;
}
.width240PX {
width: 210px;
}
}
@media (min-width: 992px) and (max-width: 1080px) {
.width1015PX { width: 850px; }
.width645PX { width: 525px; }
.width340PX { width: 300px; }
.width240PX { width: 100px; }
.width1015PX {
width: 850px;
}
.width645PX {
width: 525px;
}
.width340PX {
width: 300px;
}
.width240PX {
width: 100px;
}
}
@media (min-width: 0px) and (max-width: 992px) {
.width1015PX { width: 600px; }
.width645PX { width: 600px; }
.width340PX { width: 0px; }
.width240PX { width: 100px; }
.width1015PX {
max-width: 600px;
width: 100%;
}
.width645PX {
max-width: 600px;
width: 100%;
}
.width340PX {
width: 0px;
}
.width240PX {
width: 0px;
}
}
.top0 { top: 0; }
.top60PC { top: 60%; }
.top0 {
top: 0;
}
.textAlignLeft { text-align: left; }
.textAlignCenter { text-align: center; }
.top60PC {
top: 60%;
}
.fontSize24PX { font-size: 24px; }
.fontSize19PX { font-size: 19px; }
.fontSize16PX { font-size: 16px; }
.fontSize15PX { font-size: 15px; }
.fontSize14PX { font-size: 14px; }
.fontSize13PX { font-size: 13px; }
.fontSize12PX { font-size: 12px; }
.textAlignLeft {
text-align: left;
}
.fontWeightNormal { font-weight: 400; }
.fontWeightMedium { font-weight: 500; }
.fontWeightBold { font-weight: 600; }
.fontWeightExtraBold { font-weight: 800; }
.textAlignCenter {
text-align: center;
}
.noUnderline { text-decoration: none; }
.underline { text-decoration: underline; }
.underline_onHover:hover { text-decoration: underline; }
.fontSize24PX {
font-size: 24px;
}
.objectFitCover { object-fit: cover; }
.fontSize19PX {
font-size: 19px;
}
.z1 { z-index: 1; }
.z2 { z-index: 2; }
.z3 { z-index: 3; }
.z4 { z-index: 4; }
.fontSize16PX {
font-size: 16px;
}
.fontSize15PX {
font-size: 15px;
}
.fontSize14PX {
font-size: 14px;
}
.fontSize13PX {
font-size: 13px;
}
.fontSize12PX {
font-size: 12px;
}
.fontWeightNormal {
font-weight: 400;
}
.fontWeightMedium {
font-weight: 500;
}
.fontWeightBold {
font-weight: 600;
}
.fontWeightExtraBold {
font-weight: 800;
}
.noUnderline {
text-decoration: none;
}
.underline {
text-decoration: underline;
}
.underline_onHover:hover {
text-decoration: underline;
}
.objectFitCover {
object-fit: cover;
}
.shadow {
/* todo */
}
.z1 {
z-index: 1;
}
.z2 {
z-index: 2;
}
.z3 {
z-index: 3;
}
.z4 {
z-index: 4;
}
.marginVertical5PX {
margin-top: 5px;
margin-bottom: 5px;
}
.marginLeft5PX { margin-left: 5px; }
.marginRight2PX { margin-right: 2px; }
.marginRight5PX { margin-right: 5px; }
.marginLeft5PX {
margin-left: 5px;
}
.marginRight2PX {
margin-right: 2px;
}
.marginRight5PX {
margin-right: 5px;
}
.marginVertical10PX {
margin-top: 10px;
margin-bottom: 10px;
}
.marginRight10PX { margin-right: 10px; }
.marginLeft15PX { margin-left: 15px; }
.marginLeftAuto { margin-left: auto; }
.marginRightAuto { margin-right: auto; }
.marginBottom15PX { margin-bottom: 15px; }
.marginBottom10PX { margin-bottom: 10px; }
.marginTop10PX { margin-top: 10px; }
.marginTop5PX { margin-top: 5px; }
.marginTopAuto { margin-top: auto; }
.marginTopNeg30PX { margin-top: -30px; }
.marginRight10PX {
margin-right: 10px;
}
.paddingTop5625PC { padding-top: 56.25%; }
.marginLeft15PX {
margin-left: 15px;
}
.marginLeftAuto {
margin-left: auto;
}
.marginRightAuto {
margin-right: auto;
}
.marginBottom15PX {
margin-bottom: 15px;
}
.marginBottom10PX {
margin-bottom: 10px;
}
.marginTop10PX {
margin-top: 10px;
}
.marginTop5PX {
margin-top: 5px;
}
.marginTopAuto {
margin-top: auto;
}
.marginTopNeg30PX {
margin-top: -30px;
}
.marginTopNeg75PX {
margin-top: -75px;
}
.paddingTop5625PC {
padding-top: 56.25%;
}
.paddingTop10PX {
padding-top: 10px;
}
.paddingHorizontal15PX {
padding-left: 15px;
padding-right: 15px;
}
.paddingLeft15PX { padding-left: 15px; }
.paddingRight15PX { padding-right: 15px; }
.paddingLeft15PX {
padding-left: 15px;
}
.paddingRight15PX {
padding-right: 15px;
}
.paddingVertical5PX {
padding-top: 5px;
@ -315,6 +773,10 @@ body {
padding-bottom: 2px;
}
.paddingBottom15PX {
padding-bottom: 15px;
}
.paddingHorizontal5PX {
padding-left: 5px;
padding-right: 5px;
@ -328,4 +790,4 @@ body {
.paddingHorizontal20PX {
padding-left: 20px;
padding-right: 20px;
}
}

View File

@ -10,6 +10,7 @@
"build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack",
"manage:translations": "node ./config/webpack/translationRunner.js",
"start": "node ./streaming/index.js",
"storybook": "start-storybook",
"test": "${npm_execpath} run test:lint && ${npm_execpath} run test:jest",
"test:lint": "eslint --ext=js .",
"test:jest": "cross-env NODE_ENV=test jest --coverage",
@ -74,6 +75,7 @@
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.3.4",
"@clusterws/cws": "^0.14.0",
"@storybook/react": "^5.3.14",
"array-includes": "^3.0.3",
"autoprefixer": "^9.5.1",
"axios": "^0.19.0",

3916
yarn.lock

File diff suppressed because it is too large Load Diff