This commit is contained in:
mgabdev 2020-03-27 11:29:52 -04:00
parent 3d0a85cde4
commit 2bd344a594
46 changed files with 545 additions and 1448 deletions

View File

@ -1,3 +1,4 @@
// : todo : fill, stroke colors
const GlobeIcon = ({
className = '',
width = '12px',

View File

@ -139,6 +139,7 @@ export default class Button extends PureComponent {
backgroundColorBrand_onHover: color === COLORS.brand && outline,
colorWhite_onHover: !!children && color === COLORS.brand && outline,
fillColorSecondary: !!icon && color === COLORS.secondary,
fillColorWhite: !!icon && color === COLORS.white,
fillColorBrand: !!icon && color === COLORS.brand,
fillColorWhite_onHover: !!icon && color === COLORS.brand && outline,

View File

@ -58,7 +58,7 @@ class ModalLayout extends PureComponent {
const childrenContainerClasses = cx({
default: 1,
heightMax80VH: 1,
overflowScroll: 1,
overflowYScroll: 1,
px15: !noPadding,
py10: !noPadding,
})
@ -77,6 +77,7 @@ class ModalLayout extends PureComponent {
title={intl.formatMessage(messages.close)}
className={_s.marginLeftAuto}
onClick={this.onHandleCloseModal}
color='secondary'
icon='close'
iconWidth='10px'
iconWidth='10px'

View File

@ -2,8 +2,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { NavLink } from 'react-router-dom';
import DisplayName from '../../../../components/display_name';
import Icon from '../../../../components/icon';
import DisplayName from './display_name';
import Icon from './icon';
export default class MovedNote extends ImmutablePureComponent {

View File

@ -8,6 +8,8 @@ import Button from '../button'
import Divider from '../divider'
import Heading from '../heading'
import Icon from '../icon'
import Input from '../input'
import Switch from '../switch'
import Text from '../text'
const messages = defineMessages({
@ -33,11 +35,26 @@ class NotificationFilterPanel extends ImmutablePureComponent {
return (
<PanelLayout title={intl.formatMessage(messages.title)}>
<Text>Date</Text>
<Text>Verified</Text>
<Text>Users</Text>
<Text>Status Id</Text>
<Text>Only People I Follow</Text>
<Text>Start Date</Text>
<Text>End Date</Text>
<Divider small />
<Input title='From specific user(s)' small />
<Input title='From a specific status' small />
<Divider small />
<Switch
id='notifications-verified'
label='Only Verified Users'
/>
<Switch
id='notifications-verified'
label='Only People I Follow'
/>
</PanelLayout>
)
}

View File

@ -54,10 +54,10 @@ class TrendsPanel extends ImmutablePureComponent {
{ /* trends && trends.map(hashtag => (
<TrendingItem key={hashtag.get('name')} hashtag={hashtag} />
)) */ }
<TrendingItem />
<TrendingItem />
<TrendingItem />
<TrendingItem />
<TrendingItem index='1' />
<TrendingItem index='2' />
<TrendingItem index='3' />
<TrendingItem index='4' />
</div>
</PanelLayout>
)

View File

@ -1,11 +1,14 @@
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
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 { EmojiPicker as EmojiPickerAsync } from '../../../ui/util/async_components';
import { buildCustomEmojis } from '../../../../components/emoji/emoji';
import { defineMessages, injectIntl } from 'react-intl'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { Map as ImmutableMap } from 'immutable'
import classNames from 'classnames'
import { createSelector } from 'reselect'
import detectPassiveEvents from 'detect-passive-events'
import { changeSetting } from '../../actions/settings'
import { useEmoji } from '../../actions/emojis'
import { EmojiPicker as EmojiPickerAsync } from '../../features/ui/util/async_components'
import { buildCustomEmojis } from '../emoji/emoji'
const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
@ -22,13 +25,13 @@ const messages = defineMessages({
objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
});
})
const assetHost = process.env.CDN_HOST || '';
let EmojiPicker, Emoji; // load asynchronously
const assetHost = process.env.CDN_HOST || ''
let EmojiPicker, Emoji // load asynchronously
const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`;
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false
const categoriesSort = [
'recent',
@ -93,9 +96,9 @@ class ModifierPickerMenu extends PureComponent {
return (
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
<button onClick={this.handleClick} data-index={1}>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} />
{/*<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} />*/}
</button>
<button onClick={this.handleClick} data-index={2}>
{/*<button onClick={this.handleClick} data-index={2}>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} />
</button>
<button onClick={this.handleClick} data-index={3}>
@ -109,7 +112,7 @@ class ModifierPickerMenu extends PureComponent {
</button>
<button onClick={this.handleClick} data-index={6}>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} />
</button>
</button>*/}
</div>
);
}
@ -144,7 +147,7 @@ class ModifierPicker extends PureComponent {
return (
<div className='emoji-picker-dropdown__modifiers'>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
{ /* <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} /> */ }
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
</div>
);
@ -162,7 +165,6 @@ class EmojiPickerMenu extends ImmutablePureComponent {
onClose: PropTypes.func.isRequired,
onPick: PropTypes.func.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
intl: PropTypes.object.isRequired,
@ -178,7 +180,6 @@ class EmojiPickerMenu extends ImmutablePureComponent {
state = {
modifierOpen: false,
placement: null,
};
handleDocumentClick = e => {
@ -252,7 +253,7 @@ class EmojiPickerMenu extends ImmutablePureComponent {
}
const title = intl.formatMessage(messages.emoji);
const { modifierOpen } = this.state;
const { modifierOpen } = this.state
return (
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
@ -289,9 +290,85 @@ class EmojiPickerMenu extends ImmutablePureComponent {
}
const perLine = 8
const lines = 2
const DEFAULTS = [
'+1',
'grinning',
'kissing_heart',
'heart_eyes',
'laughing',
'stuck_out_tongue_winking_eye',
'sweat_smile',
'joy',
'yum',
'disappointed',
'thinking_face',
'weary',
'sob',
'sunglasses',
'heart',
'ok_hand',
];
const getFrequentlyUsedEmojis = createSelector([
state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
], emojiCounters => {
let emojis = emojiCounters
.keySeq()
.sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b))
.reverse()
.slice(0, perLine * lines)
.toArray();
if (emojis.length < DEFAULTS.length) {
let uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length));
}
return emojis;
});
const getCustomEmojis = createSelector([
state => state.get('custom_emojis'),
], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
const aShort = a.get('shortcode').toLowerCase();
const bShort = b.get('shortcode').toLowerCase();
if (aShort < bShort) {
return -1;
} else if (aShort > bShort ) {
return 1;
}
return 0;
}));
const mapStateToProps = state => ({
custom_emojis: getCustomEmojis(state),
skinTone: state.getIn(['settings', 'skinTone']),
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
});
const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
onSkinTone: skinTone => {
dispatch(changeSetting(['skinTone'], skinTone));
},
onPickEmoji: emoji => {
dispatch(useEmoji(emoji));
if (onPickEmoji) {
onPickEmoji(emoji);
}
},
});
export default
@injectIntl
class EmojiPickerDropdown extends ImmutablePureComponent {
@connect(mapStateToProps, mapDispatchToProps)
class EmojiPickerPopover extends ImmutablePureComponent {
static propTypes = {
custom_emojis: ImmutablePropTypes.list,
@ -307,11 +384,7 @@ class EmojiPickerDropdown extends ImmutablePureComponent {
loading: false,
};
setRef = (c) => {
this.dropdown = c;
}
onShowDropdown = ({ target }) => {
componentWillMount = () => {
this.setState({ active: true });
if (!EmojiPicker) {
@ -326,9 +399,6 @@ class EmojiPickerDropdown extends ImmutablePureComponent {
this.setState({ loading: false });
});
}
const { top } = target.getBoundingClientRect();
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
}
onHideDropdown = () => {
@ -351,30 +421,19 @@ class EmojiPickerDropdown extends ImmutablePureComponent {
}
}
setTargetRef = c => {
this.target = c;
}
findTarget = () => {
return this.target;
}
render () {
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
const title = intl.formatMessage(messages.emoji);
const { active, loading, placement } = this.state;
const {
intl,
onPickEmoji,
onSkinTone,
skinTone,
frequentlyUsedEmojis
} = this.props
const { active, loading } = this.state;
return (
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
<div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
<img
className={classNames('emojione', { 'pulse-loading': active && loading })}
alt='🙂'
src={`${assetHost}/emoji/1f602.svg`}
/>
</div>
<Overlay show={active} placement={placement} target={this.findTarget}>
<EmojiPickerMenu
custom_emojis={this.props.custom_emojis}
loading={loading}
@ -384,7 +443,6 @@ class EmojiPickerDropdown extends ImmutablePureComponent {
skinTone={skinTone}
frequentlyUsedEmojis={frequentlyUsedEmojis}
/>
</Overlay>
</div>
);
}

View File

@ -5,6 +5,7 @@ import BundleModalError from '../bundle_modal_error'
import PopoverBase from './popover_base'
import ContentWarningPopover from './content_warning_popover'
import DatePickerPopover from './date_picker_popover'
import EmojiPickerPopover from './emoji_picker_popover'
import GroupInfoPopover from './group_info_popover'
import ProfileOptionsPopover from './profile_options_popover'
import SearchPopover from './search_popover'
@ -19,6 +20,7 @@ const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : fal
const POPOVER_COMPONENTS = {
CONTENT_WARNING: () => Promise.resolve({ default: ContentWarningPopover }),
DATE_PICKER: () => Promise.resolve({ default: DatePickerPopover }),
EMOJI_PICKER: () => Promise.resolve({ default: EmojiPickerPopover }),
GROUP_INFO: () => GroupInfoPopover,
PROFILE_OPTIONS: () => Promise.resolve({ default: ProfileOptionsPopover }),
SEARCH: () => Promise.resolve({ default: SearchPopover }),

View File

@ -1,259 +0,0 @@
import { injectIntl, defineMessages } from 'react-intl'
import spring from 'react-motion/lib/spring'
import detectPassiveEvents from 'detect-passive-events'
import classNames from 'classnames'
import Overlay from 'react-overlays/lib/Overlay'
import { changeComposeVisibility } from '../../actions/compose'
import { openModal, closeModal } from '../../actions/modal'
import { isUserTouching } from '../../utils/is_mobile'
import Motion from '../../features/ui/util/optional_motion'
import Icon from '../icon'
import ComposeExtraButton from '../../features/compose/components/compose_extra_button'
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
visibility: { id: 'privacy.visibility', defaultMessage: 'Visibility' },
})
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false
class PrivacyDropdownMenu extends PureComponent {
static propTypes = {
style: PropTypes.object,
items: PropTypes.array.isRequired,
value: PropTypes.string.isRequired,
placement: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
}
state = {
mounted: false,
}
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose()
}
}
handleKeyDown = e => {
const { items } = this.props
const value = e.currentTarget.getAttribute('data-index')
const index = items.findIndex(item => {
return (item.value === value)
})
let element
switch(e.key) {
case 'Escape':
this.props.onClose()
break
case 'Enter':
this.handleClick(e)
break
case 'ArrowDown':
element = this.node.childNodes[index + 1]
if (element) {
element.focus()
this.props.onChange(element.getAttribute('data-index'))
}
break
case 'ArrowUp':
element = this.node.childNodes[index - 1]
if (element) {
element.focus()
this.props.onChange(element.getAttribute('data-index'))
}
break
case 'Home':
element = this.node.firstChild
if (element) {
element.focus()
this.props.onChange(element.getAttribute('data-index'))
}
break
case 'End':
element = this.node.lastChild
if (element) {
element.focus()
this.props.onChange(element.getAttribute('data-index'))
}
break
}
}
handleClick = e => {
const value = e.currentTarget.getAttribute('data-index')
e.preventDefault()
this.props.onClose()
this.props.onChange(value)
}
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false)
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions)
if (this.focusedItem) this.focusedItem.focus()
this.setState({ mounted: true })
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false)
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions)
}
setRef = c => {
this.node = c
}
setFocusRef = c => {
this.focusedItem = c
}
render () {
const { mounted } = this.state
const { style, items, placement, value } = this.props
return (
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} 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={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}>
{items.map(item => (
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
<div className='privacy-dropdown__option__icon'>
<Icon id={item.icon} fixedWidth />
</div>
<div className='privacy-dropdown__option__content'>
<strong>{item.text}</strong>
{item.meta}
</div>
</div>
))}
</div>
)}
</Motion>
)
}
}
const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS',
value: state.getIn(['compose', 'privacy']),
})
const mapDispatchToProps = dispatch => ({
onChange (value) {
dispatch(changeComposeVisibility(value))
},
isUserTouching,
onModalOpen: props => dispatch(openModal('ACTIONS', props)),
onModalClose: () => dispatch(closeModal()),
})
export default
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class PrivacyDropdown extends PureComponent {
static propTypes = {
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
}
state = {
open: false,
placement: 'bottom',
}
handleToggle = ({ target }) => {
if (this.props.isUserTouching()) {
if (this.state.open) {
this.props.onModalClose()
} else {
this.props.onModalOpen({
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
onClick: this.handleModalActionClick,
})
}
} else {
const { top } = target.getBoundingClientRect()
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' })
this.setState({ open: !this.state.open })
}
}
handleModalActionClick = (e) => {
e.preventDefault()
const { value } = this.options[e.currentTarget.getAttribute('data-index')]
this.props.onModalClose()
this.props.onChange(value)
}
handleKeyDown = e => {
switch(e.key) {
case 'Escape':
this.handleClose()
break
}
}
handleClose = () => {
this.setState({ open: false })
}
handleChange = value => {
this.props.onChange(value)
}
componentWillMount () {
const { intl: { formatMessage } } = this.props
this.options = [
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
]
}
render () {
const { value, intl } = this.props
const { open, placement } = this.state
const valueOption = this.options.find(item => item.value === value)
return (
<PrivacyDropdownMenu
items={this.options}
value={value}
onClose={this.handleClose}
onChange={this.handleChange}
placement={placement}
/>
)
}
}

View File

@ -1,9 +1,129 @@
export default class StatusVisibilityPopover extends PureComponent {
render() {
import { injectIntl, defineMessages } from 'react-intl'
import classNames from 'classnames/bind'
import { changeComposeVisibility } from '../../actions/compose'
import { closePopover } from '../../actions/popover'
import PopoverLayout from './popover_layout'
import Icon from '../icon'
import Text from '../text'
const cx = classNames.bind(_s)
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
visibility: { id: 'privacy.visibility', defaultMessage: 'Visibility' },
})
const mapStateToProps = state => ({
value: state.getIn(['compose', 'privacy']),
})
const mapDispatchToProps = dispatch => ({
onChange (value) {
dispatch(changeComposeVisibility(value))
dispatch(closePopover())
},
})
export default
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class StatusVisibilityDropdown extends PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
}
handleChange = value => {
this.props.onChange(value)
}
componentWillMount () {
const { intl } = this.props
this.options = [
{
icon: 'globe',
value: 'public',
title: intl.formatMessage(messages.public_short),
subtitle: intl.formatMessage(messages.public_long)
},
{
icon: 'unlock',
value: 'unlisted',
title: intl.formatMessage(messages.unlisted_short),
subtitle: intl.formatMessage(messages.unlisted_long)
},
{
icon: 'lock',
value: 'private',
title: intl.formatMessage(messages.private_short),
subtitle: intl.formatMessage(messages.private_long)
},
]
}
render () {
const { value } = this.props
return (
<div>
{ /* */ }
<PopoverLayout className={_s.width240PX}>
<div className={[_s.default].join(' ')}>
{
this.options.map((option, i) => {
const isActive = option.value === value
const isLast = i === this.options.length - 1
const containerClasses = cx({
default: 1,
flexRow: 1,
py10: 1,
cursorPointer: 1,
borderBottom1PX: !isLast,
borderColorSecondary: !isLast,
backgroundSubtle_onHover: !isActive,
backgroundColorBrand: isActive,
})
const iconClasses = cx({
ml10: 1,
mt2: 1,
fillColorWhite: isActive,
})
return (
<div
role='button'
onClick={() => this.handleChange(option.value)}
className={containerClasses}
>
<Icon id={option.icon} height='16px' width='16px' className={iconClasses} />
<div className={[_s.default, _s.px10, _s.pt2].join(' ')}>
<Text size='medium' color={isActive ? 'white' : 'primary'}>
{option.title}
</Text>
<Text size='small' weight='medium' color={isActive ? 'white' : 'secondary'}>
{option.subtitle}
</Text>
</div>
</div>
)
})
}
</div>
</PopoverLayout>
)
}
}

View File

@ -242,6 +242,8 @@ class ProfileHeader extends ImmutablePureComponent {
console.log("buttonOptions:", buttonText, buttonOptions)
// : todo : "follows you", "mutual follow"
return (
<div className={[_s.default, _s.z1, _s.width100PC].join(' ')}>

View File

@ -103,7 +103,7 @@ export default class SidebarSectionItem extends PureComponent {
buttonRef={buttonRef}
onMouseEnter={() => this.handleOnMouseEnter()}
onMouseLeave={() => this.handleOnMouseLeave()}
className={[_s.default, _s.noUnderline, _s.cursorPointer, _s.width100PC, _s.alignItemsStart, _s.backgroundTransparent].join(' ')}
className={[_s.default, _s.noUnderline, _s.cursorPointer, _s.width100PC, _s.backgroundTransparent].join(' ')}
>
<div className={containerClasses}>
<div className={[_s.default]}>

View File

@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import punycode from 'punycode';
import classnames from 'classnames';
import Icon from '../../../../components/icon';
import Icon from './icon';
const IDNA_PREFIX = 'xn--';

View File

@ -285,12 +285,18 @@ class StatusContent extends ImmutablePureComponent {
/>
{
this.state.collapsed &&
<button
className={[_s.default, _s.displayFlex, _s.cursorPointer, _s.py2, _s.text, _s.colorPrimary, _s.fontWeightBold, _s.fontSize15PX].join(' ')}
<Button
text
underlineOnHover
color='primary'
backgroundColor='none'
className={[_s.py2].join(' ')}
onClick={this.handleReadMore}
>
<Text size='medium' color='inherit' weight='bold'>
{intl.formatMessage(messages.readMore)}
</button>
</Text>
</Button>
}
</div>
)

View File

@ -15,6 +15,7 @@ const cx = classNames.bind(_s)
export default class TrendingItem extends ImmutablePureComponent {
static propTypes = {
index: PropTypes.number,
trend: ImmutablePropTypes.map.isRequired,
}
@ -31,7 +32,7 @@ export default class TrendingItem extends ImmutablePureComponent {
}
render() {
const { trend } = this.props
const { trend, index } = this.props
const { hovering } = this.state
const subtitleClasses = cx({
@ -44,7 +45,7 @@ export default class TrendingItem extends ImmutablePureComponent {
underline: hovering,
})
return null;
// return null;
// : todo :
@ -56,7 +57,7 @@ export default class TrendingItem extends ImmutablePureComponent {
onMouseLeave={() => this.handleOnMouseLeave()}
>
<div className={[_s.default, _s.flexRow, _s.mt5].join(' ')}>
<Text size='small' color='secondary'>1</Text>
<Text size='small' color='secondary'>{index}</Text>
<DotTextSeperator />
<Text size='small' color='secondary' className={_s.ml5}>Politics</Text>
</div>

View File

@ -58,7 +58,7 @@ export default class TrendingItemCard extends ImmutablePureComponent {
The best flower subscription services: BloomsyBox, Bouqs...
</Text>
</div>
<Image width='92px' height='92px' />
<Image width='92px' height='96px' />
</div>
)
}

View File

@ -452,6 +452,8 @@ class Video extends PureComponent {
// fullscreen
// })}
// : todo spoiler :
const seekHandleClasses = cx({
default: 1,
positionAbsolute: 1,

View File

@ -2,9 +2,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { List as ImmutableList } from 'immutable'
import { injectIntl, defineMessages } from 'react-intl'
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines'
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs'
import StatusList from '../../components/status_list'
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../actions/timelines'
import { fetchAccountIdentityProofs } from '../actions/identity_proofs'
import StatusList from '../components/status_list'
const messages = defineMessages({
empty: { id: 'empty_column.account_timeline', defaultMessage: 'No gabs here!' },

View File

@ -1,105 +0,0 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import InnerHeader from '../inner_header';
import MovedNote from '../moved_note';
export default class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
identity_proofs: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onRepostToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
domain: PropTypes.string.isRequired,
username: PropTypes.string,
};
static contextTypes = {
router: PropTypes.object,
};
handleFollow = () => {
this.props.onFollow(this.props.account);
}
handleBlock = () => {
this.props.onBlock(this.props.account);
}
handleMention = () => {
this.props.onMention(this.props.account, this.context.router.history);
}
handleReport = () => {
this.props.onReport(this.props.account);
}
handleRepostToggle = () => {
this.props.onRepostToggle(this.props.account);
}
handleMute = () => {
this.props.onMute(this.props.account);
}
handleBlockDomain = () => {
const domain = this.props.account.get('acct').split('@')[1];
if (!domain) return;
this.props.onBlockDomain(domain);
}
handleUnblockDomain = () => {
const domain = this.props.account.get('acct').split('@')[1];
if (!domain) return;
this.props.onUnblockDomain(domain);
}
handleEndorseToggle = () => {
this.props.onEndorseToggle(this.props.account);
}
handleAddToList = () => {
this.props.onAddToList(this.props.account);
}
render () {
const { account, identity_proofs } = this.props;
const moved = (account) ? account.get('moved') : false;
return (
<div className='account-timeline__header'>
{ moved && <MovedNote from={account} to={account.get('moved')} /> }
<InnerHeader
account={account}
identity_proofs={identity_proofs}
onFollow={this.handleFollow}
onBlock={this.handleBlock}
onMention={this.handleMention}
onRepostToggle={this.handleRepostToggle}
onReport={this.handleReport}
onMute={this.handleMute}
onBlockDomain={this.handleBlockDomain}
onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle}
onAddToList={this.handleAddToList}
domain={this.props.domain}
username={this.props.username}
/>
</div>
);
}
}

View File

@ -1,6 +0,0 @@
.account-timeline {
&__header {
display: block;
width: 100%;
}
}

View File

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

View File

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

View File

@ -1,337 +0,0 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { NavLink } from 'react-router-dom';
import { debounce } from 'lodash';
import Button from '../../../components/button';
import { autoPlayGif, me, isStaff } from '../../../initial_state';
import Avatar from '../../../components/avatar';
import { shortNumberFormat } from '../../../utils/numbers';
import ProfileInfoPanel from './profile_info_panel/profile_info_panel';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
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.' },
mention: { id: 'account.mention', defaultMessage: 'Mention' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
report: { id: 'account.report', defaultMessage: 'Report @{name}' },
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
media: { id: 'account.media', defaultMessage: 'Media' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
hideReposts: { id: 'account.hide_reblogs', defaultMessage: 'Hide reposts from @{name}' },
showReposts: { id: 'account.show_reblogs', defaultMessage: 'Show reposts from @{name}' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
accountFollowsYou: { id: 'account.follows_you', defaultMessage: 'Follows you' },
accountBlocked: { id: 'account.blocked', defaultMessage: 'Blocked' },
accountMuted: { id: 'account.muted', defaultMessage: 'Muted' },
domainBlocked: { id: 'account.domain_blocked', defaultMessage: 'Domain hidden' },
});
export default
@injectIntl
class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
identity_props: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired,
username: PropTypes.string,
};
state = {
isSmallScreen: (window.innerWidth <= 895),
}
openEditProfile = () => {
window.open('/settings/profile', '_blank');
}
isStatusesPageActive = (match, location) => {
if (!match) {
return false;
}
return !location.pathname.match(/\/(followers|following|favorites|pins)\/?$/);
}
componentWillMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
}
componentWillUnmount () {
window.removeEventListener('resize', this.handleResize);
}
onChat = () => {
const { account } = this.props;
axios.post('https://chat.gab.com/private-message', {
username: account.get('username'),
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
}
handleResize = debounce(() => {
this.setState({ isSmallScreen: (window.innerWidth <= 895) });
}, 5, {
trailing: true,
});
makeMenu() {
const { account, intl, domain } = this.props;
let menu = [];
if (!account || !me) {
return [];
}
if ('share' in navigator) {
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
menu.push(null);
}
if (account.get('id') === me) {
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('acct') }), action: this.props.onMention });
if (account.getIn(['relationship', 'following'])) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
menu.push({ text: intl.formatMessage(messages.hideReposts, { name: account.get('username') }), action: this.props.onRepostToggle });
} else {
menu.push({ text: intl.formatMessage(messages.showReposts, { name: account.get('username') }), action: this.props.onRepostToggle });
}
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
menu.push(null);
}
if (account.getIn(['relationship', 'muting'])) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
} else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute });
}
if (account.getIn(['relationship', 'blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
} else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
}
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
}
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
menu.push(null);
if (account.getIn(['relationship', 'domain_blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain });
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain });
}
}
if (account.get('id') !== me && isStaff) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
}
return menu;
}
makeInfo() {
const { account, intl } = this.props;
let info = [];
if (!account || !me) return info;
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
info.push(<span key='followed_by' className='relationship-tag'>{intl.formatMessage(messages.accountFollowsYou)}</span>);
} else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
info.push(<span key='blocked' className='relationship-tag'>{intl.formatMessage(messages.accountBlocked)}</span>);
}
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
info.push(<span key='muted' className='relationship-tag'>{intl.formatMessage(messages.accountMuted)}</span>);
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
info.push(<span key='domain_blocked' className='relationship-tag'>{intl.formatMessage(messages.domainBlocked)}</span>);
}
return info;
};
getActionBtn() {
const { account, intl } = this.props;
let actionBtn = null;
if (!account || !me) return actionBtn;
if (me !== account.get('id')) {
if (!account.get('relationship')) { // Wait until the relationship is loaded
//
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}
}
return actionBtn;
};
render () {
const { account, intl, domain, username } = this.props;
const { isSmallScreen } = this.state;
if (!account) {
return (
<div className='account__header'>
<div className='account__header__image account__header__image--none'></div>
<div className='account__header__bar'>
<div className='account__header__extra'>
<div className='account__header__avatar'></div>
</div>
{
isSmallScreen &&
<div className='account-mobile-container account-mobile-container--nonuser'>
<ProfileInfoPanel username={username} />
</div>
}
</div>
</div>
);
}
const info = this.makeInfo();
const actionBtn = this.getActionBtn();
const menu = this.makeMenu();
const content = { __html: account.get('note_emojified') };
const fields = account.get('fields');
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
const headerImgSrc = autoPlayGif ? account.get('header') : account.get('header_static');
const headerMissing = (headerImgSrc.indexOf('/headers/original/missing.png') > -1);
const avatarSize = isSmallScreen ? 90 : 200;
return (
<div className={classNames('account__header', { inactive: !!account.get('moved') })}>
<div className={classNames('account__header__image', { 'account__header__image--none': headerMissing })}>
<div className='account__header__info'>
{info}
</div>
<img src={headerImgSrc} alt='' className='parallax' />
</div>
<div className='account__header__bar'>
<div className='account__header__extra'>
<div className='account__header__avatar'>
<Avatar account={account} size={avatarSize} />
</div>
<div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
{shortNumberFormat(account.get('statuses_count'))}
<FormattedMessage id='account.posts' defaultMessage='Gabs' />
</NavLink>
<NavLink exact activeClassName='active' to={`/${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
{shortNumberFormat(account.get('following_count'))}
<FormattedMessage id='account.follows' defaultMessage='Follows' />
</NavLink>
<NavLink exact activeClassName='active' to={`/${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
{shortNumberFormat(account.get('followers_count'))}
<FormattedMessage id='account.followers' defaultMessage='Followers' />
</NavLink>
{
account.get('id') === me &&
<div>
<NavLink exact activeClassName='active' to={`/${account.get('acct')}/favorites`} title={intl.formatNumber(account.get('favourite_count'))}>
{ /* : TODO : shortNumberFormat(account.get('favourite_count')) */ }
<span></span>
<FormattedMessage id='navigation_bar.favorites' defaultMessage='Favorites' />
</NavLink>
<NavLink exact activeClassName='active' to={`/${account.get('acct')}/pins`} title={intl.formatNumber(account.get('pinned_count'))}>
{ /* : TODO : shortNumberFormat(account.get('pinned_count')) */ }
<span></span>
<FormattedMessage id='navigation_bar.pins' defaultMessage='Pins' />
</NavLink>
</div>
}
</div>
{
isSmallScreen &&
<div className='account-mobile-container'>
<ProfileInfoPanel username={username} account={account} />
</div>
}
{
me &&
<div className='account__header__extra__buttons'>
{actionBtn}
{account.get('id') !== me &&
<Button className='button button--alternative-2' onClick={this.props.onMention}>
<FormattedMessage id='account.mention' defaultMessage='Mention' values={{
name: account.get('acct')
}} />
</Button>
}
{/*<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />*/}
</div>
}
</div>
</div>
</div>
);
}
}

View File

@ -1,17 +0,0 @@
.relationship-tag {
display: block;
margin-bottom: 4px;
vertical-align: top;
color: $primary-text-color;
background-color: $base-overlay-background;
text-transform: uppercase;
padding: 4px;
border-radius: 4px;
opacity: 0.7;
@include text-sizing(11px, 500);
&:hover {
opacity: 1;
}
}

View File

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

View File

@ -1,55 +0,0 @@
.moved-note {
padding: 14px 10px 16px 10px;
background: lighten($ui-base-color, 4%);
border-top: 1px solid lighten($ui-base-color, 8%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
&__message {
position: relative;
margin-left: 58px;
color: $dark-text-color;
padding-bottom: 4px;
font-size: 14px;
>span {
display: block;
@include text-overflow;
}
}
&__icon-wrapper {
left: -26px;
position: absolute;
}
&__display-name {
display: block;
color: $secondary-text-color;
margin-bottom: 0;
line-height: 24px;
overflow: hidden;
strong,
span {
display: block;
@include text-overflow;
}
strong {
font-size: 16px;
color: $primary-text-color;
}
&:hover strong {
text-decoration: underline;
}
}
&__display-avatar {
position: relative;
float: left;
margin-right: 10px;
}
}

View File

@ -1,117 +0,0 @@
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { List as ImmutableList } from 'immutable';
import {
followAccount,
unfollowAccount,
blockAccount,
unblockAccount,
unmuteAccount,
pinAccount,
unpinAccount,
} from '../../../actions/accounts';
import {
mentionCompose,
} from '../../../actions/compose';
import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports';
import { openModal } from '../../../actions/modal';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
import { unfollowModal } from '../../../initial_state';
import { makeGetAccount } from '../../../selectors';
import Header from '../components/header';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
domain: state.getIn(['meta', 'domain']),
identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()),
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (unfollowModal) {
dispatch(openModal('UNFOLLOW', {
accountId: account.get('id'),
}))
} else {
dispatch(unfollowAccount(account.get('id')))
}
} else {
dispatch(followAccount(account.get('id')))
}
},
onBlock (account) {
if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id')));
} else {
dispatch(openModal('BLOCK_ACCOUNT', {
accountId: account.get('id'),
}));
}
},
onMention (account, router) {
dispatch(mentionCompose(account, router));
},
onRepostToggle (account) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
dispatch(followAccount(account.get('id'), false));
} else {
dispatch(followAccount(account.get('id'), true));
}
},
onEndorseToggle (account) {
if (account.getIn(['relationship', 'endorsed'])) {
dispatch(unpinAccount(account.get('id')));
} else {
dispatch(pinAccount(account.get('id')));
}
},
onReport (account) {
dispatch(initReport(account));
},
onMute (account) {
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
} else {
dispatch(initMuteModal(account));
}
},
onBlockDomain (domain) {
dispatch(openModal('BLOCK_DOMAIN', {
domain,
}));
},
onUnblockDomain (domain) {
dispatch(unblockDomain(domain));
},
onAddToList(account){
dispatch(openModal('LIST_ADDER', {
accountId: account.get('id'),
}));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));

View File

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

View File

@ -11,6 +11,7 @@ export default class ComposeExtraButton extends PureComponent {
icon: PropTypes.string,
small: PropTypes.bool,
active: PropTypes.bool,
buttonRef: PropTypes.func,
}
render() {
@ -21,16 +22,12 @@ export default class ComposeExtraButton extends PureComponent {
icon,
children,
small,
active
active,
buttonRef
} = this.props
const containerClasses = cx({
default: 1,
mr10: !small,
mr2: small,
})
const btnClasses = cx({
backgroundSubtle_onHover: 1,
backgroundColorBrandLight: active,
py10: !small,
px10: !small,
@ -43,16 +40,16 @@ export default class ComposeExtraButton extends PureComponent {
fillColorWhite: active,
})
const iconSize = !!small ? '12px' : '18px'
const iconSize = !!small ? '12px' : '16px'
return (
<div className={containerClasses} data-tip={title}>
<div className={[_s.default, _s.mr2].join(' ')} ref={buttonRef}>
<Button
className={btnClasses}
title={title}
disabled={disabled}
onClick={onClick}
backgroundColor='secondary'
backgroundColor='none'
iconClassName={iconClasses}
icon={icon}
iconWidth={iconSize}

View File

@ -1,29 +1,30 @@
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import ImmutablePropTypes from 'react-immutable-proptypes';
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/bind'
import CharacterCounter from '../character_counter';
import UploadForm from '../upload_form';
import ReplyIndicatorContainer from '../../containers/reply_indicator_container';
import AutosuggestTextbox from '../../../../components/autosuggest_textbox';
import PollButton from '../../components/poll_button';
import UploadButton from '../../components/upload_button';
import SpoilerButton from '../../components/spoiler_button';
import PostPrivacyButton from '../../../../components/post_privacy_button';
import CharacterCounter from '../character_counter'
import UploadForm from '../upload_form'
import ReplyIndicatorContainer from '../../containers/reply_indicator_container'
import AutosuggestTextbox from '../../../../components/autosuggest_textbox'
import PollButton from '../../components/poll_button'
import UploadButton from '../../components/upload_button'
import SpoilerButton from '../../components/spoiler_button'
import RichTextEditorButton from '../../components/rich_text_editor_button'
import GifSelectorButton from '../../components/gif_selector_button'
import StatusVisibilityButton from '../../components/status_visibility_button'
import EmojiPickerButton from '../../components/emoji_picker_button'
import EmojiPickerDropdown from '../../containers/emoji_picker_dropdown_container';
import PollFormContainer from '../../containers/poll_form_container';
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 PollFormContainer from '../../containers/poll_form_container'
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';
import { isMobile } from '../../../../utils/is_mobile'
import { countableText } from '../../util/counter'
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
const maxPostCharacterCount = 3000;
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d'
const maxPostCharacterCount = 3000
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@ -266,6 +267,7 @@ class ComposeForm extends ImmutablePureComponent {
pb10: 1,
borderBottom1PX: 1,
borderColorSecondary: 1,
mb10: 1,
displayNone: !spoiler
})
@ -303,11 +305,6 @@ 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)}
@ -340,16 +337,18 @@ class ComposeForm extends ImmutablePureComponent {
<div className={actionsContainerClasses}>
<div className={[_s.default, _s.flexRow, _s.marginRightAuto].join(' ')}>
<RichTextEditorButton small={shouldCondense} />
<UploadButton small={shouldCondense} />
{
!edit && <PollButton small={shouldCondense} />
}
{
!shouldCondense &&
<PostPrivacyButton />
<StatusVisibilityButton small={shouldCondense} />
}
<SpoilerButton small={shouldCondense} />
<SchedulePostDropdown small={shouldCondense} position={isModalOpen ? 'top' : undefined} />
<GifSelectorButton small={shouldCondense} />
<EmojiPickerButton small={shouldCondense} />
</div>
<CharacterCounter max={maxPostCharacterCount} text={text} small={shouldCondense} />

View File

@ -1,6 +1,11 @@
import { addPoll, removePoll } from '../../../actions/compose'
import { defineMessages, injectIntl } from 'react-intl'
import { openPopover } from '../../../actions/popover'
import ComposeExtraButton from './compose_extra_button'
const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
})
const mapStateToProps = state => ({
// unavailable: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
// active: state.getIn(['compose', 'poll']) !== null,
@ -8,46 +13,47 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
onClick() {
dispatch((_, getState) => {
if (getState().getIn(['compose', 'poll'])) {
dispatch(removePoll())
} else {
dispatch(addPoll())
}
})
onClick(targetRef) {
dispatch(openPopover('EMOJI_PICKER', {
targetRef,
}))
},
})
export default
@injectIntl
@connect(mapStateToProps, mapDispatchToProps)
class EmojiPickerButton extends PureComponent {
static propTypes = {
disabled: PropTypes.bool,
unavailable: PropTypes.bool,
active: PropTypes.bool,
intl: PropTypes.object.isRequired,
onClick: PropTypes.func.isRequired,
active: PropTypes.bool,
small: PropTypes.bool,
}
handleClick = () => {
this.props.onClick()
this.props.onClick(this.button)
}
setButton = (n) => {
this.button = n
}
render() {
const { active, unavailable, disabled, small } = this.props
const { active, small, intl } = this.props
if (unavailable) return null
const title = intl.formatMessage(messages.emoji)
return (
<ComposeExtraButton
disabled={disabled}
onClick={this.handleClick}
icon='happy'
title={title}
small={small}
active={active}
buttonRef={this.setButton}
/>
)
}

View File

@ -0,0 +1,53 @@
import { injectIntl, defineMessages } from 'react-intl'
import { changeComposeSpoilerness } from '../../../actions/compose'
import ComposeExtraButton from './compose_extra_button'
const messages = defineMessages({
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
unmarked: { id: 'compose_form.spoiler.unmarked', defaultMessage: 'Text is not hidden' },
title: { id: 'compose_form.spoiler.title', defaultMessage: 'Warning' },
})
const mapStateToProps = (state) => ({
active: state.getIn(['compose', 'spoiler']),
})
const mapDispatchToProps = dispatch => ({
onClick () {
dispatch(changeComposeSpoilerness())
},
})
export default
@injectIntl
@connect(mapStateToProps, mapDispatchToProps)
class SpoilerButton extends PureComponent {
static propTypes = {
active: PropTypes.bool,
intl: PropTypes.object.isRequired,
small: PropTypes.bool,
}
handleClick = (e) => {
e.preventDefault()
this.props.onClick()
}
render () {
const { active, intl, small } = this.props
return (
<ComposeExtraButton
title={intl.formatMessage(messages.title)}
icon='gif'
onClick={this.handleClick}
small={small}
active={active}
/>
)
}
}

View File

@ -1,65 +0,0 @@
import { defineMessages, injectIntl } from 'react-intl'
import { addPoll, removePoll } from '../../../actions/compose'
import ComposeExtraButton from './compose_extra_button'
const messages = defineMessages({
add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add poll' },
title: { id: 'poll_button.title', defaultMessage: 'Poll' },
remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' },
})
const mapStateToProps = state => ({
unavailable: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
active: state.getIn(['compose', 'poll']) !== null,
})
const mapDispatchToProps = dispatch => ({
onClick() {
dispatch((_, getState) => {
if (getState().getIn(['compose', 'poll'])) {
dispatch(removePoll())
} else {
dispatch(addPoll())
}
})
},
})
export default
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class PollButton extends PureComponent {
static propTypes = {
disabled: PropTypes.bool,
unavailable: PropTypes.bool,
active: PropTypes.bool,
onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
small: PropTypes.bool,
}
handleClick = () => {
this.props.onClick()
}
render() {
const { intl, active, unavailable, disabled, small } = this.props
if (unavailable) return null
return (
<ComposeExtraButton
title={intl.formatMessage(active ? messages.remove_poll : messages.title)}
disabled={disabled}
onClick={this.handleClick}
icon='poll'
small={small}
active={active}
/>
)
}
}

View File

@ -0,0 +1,53 @@
import { injectIntl, defineMessages } from 'react-intl'
import { changeComposeSpoilerness } from '../../../actions/compose'
import ComposeExtraButton from './compose_extra_button'
const messages = defineMessages({
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
unmarked: { id: 'compose_form.spoiler.unmarked', defaultMessage: 'Text is not hidden' },
title: { id: 'compose_form.spoiler.title', defaultMessage: 'Warning' },
})
const mapStateToProps = (state) => ({
active: state.getIn(['compose', 'spoiler']),
})
const mapDispatchToProps = dispatch => ({
onClick () {
dispatch(changeComposeSpoilerness())
},
})
export default
@injectIntl
@connect(mapStateToProps, mapDispatchToProps)
class SpoilerButton extends PureComponent {
static propTypes = {
active: PropTypes.bool,
intl: PropTypes.object.isRequired,
small: PropTypes.bool,
}
handleClick = (e) => {
e.preventDefault()
this.props.onClick()
}
render () {
const { active, intl, small } = this.props
return (
<ComposeExtraButton
title={intl.formatMessage(messages.title)}
icon='letter-t'
onClick={this.handleClick}
small={small}
active={active}
/>
)
}
}

View File

@ -0,0 +1,72 @@
import { defineMessages, injectIntl } from 'react-intl'
import { openPopover } from '../../../actions/popover'
import ComposeExtraButton from './compose_extra_button'
const messages = defineMessages({
visibility: { id: 'privacy.visibility', defaultMessage: 'Visibility' },
})
const mapStateToProps = (state) => ({
value: state.getIn(['compose', 'privacy']),
})
const mapDispatchToProps = (dispatch) => ({
onOpenPopover(targetRef) {
dispatch(openPopover('STATUS_VISIBILITY', {
targetRef,
}))
},
})
export default
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class StatusVisibilityButton extends PureComponent {
static propTypes = {
onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
small: PropTypes.bool,
onOpenPopover: PropTypes.func.isRequired,
}
handleOnClick = () => {
this.props.onOpenPopover(this.button)
}
setButton = (n) => {
this.button = n
}
render() {
const {
intl,
small,
value
} = this.props
let icon;
switch (value) {
case 'unlisted':
icon = 'unlock'
break;
case 'private':
icon = 'lock'
break;
default:
icon = 'globe'
break;
}
return (
<ComposeExtraButton
icon={icon}
title={intl.formatMessage(messages.visibility)}
onClick={this.handleOnClick}
small={small}
buttonRef={this.setButton}
/>
)
}
}

View File

@ -1,82 +0,0 @@
import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
import { changeSetting } from '../../../actions/settings';
import { createSelector } from 'reselect';
import { Map as ImmutableMap } from 'immutable';
import { useEmoji } from '../../../actions/emojis';
const perLine = 8;
const lines = 2;
const DEFAULTS = [
'+1',
'grinning',
'kissing_heart',
'heart_eyes',
'laughing',
'stuck_out_tongue_winking_eye',
'sweat_smile',
'joy',
'yum',
'disappointed',
'thinking_face',
'weary',
'sob',
'sunglasses',
'heart',
'ok_hand',
];
const getFrequentlyUsedEmojis = createSelector([
state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
], emojiCounters => {
let emojis = emojiCounters
.keySeq()
.sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b))
.reverse()
.slice(0, perLine * lines)
.toArray();
if (emojis.length < DEFAULTS.length) {
let uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length));
}
return emojis;
});
const getCustomEmojis = createSelector([
state => state.get('custom_emojis'),
], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
const aShort = a.get('shortcode').toLowerCase();
const bShort = b.get('shortcode').toLowerCase();
if (aShort < bShort) {
return -1;
} else if (aShort > bShort ) {
return 1;
} else {
return 0;
}
}));
const mapStateToProps = state => ({
custom_emojis: getCustomEmojis(state),
skinTone: state.getIn(['settings', 'skinTone']),
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
});
const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
onSkinTone: skinTone => {
dispatch(changeSetting(['skinTone'], skinTone));
},
onPickEmoji: emoji => {
dispatch(useEmoji(emoji));
if (onPickEmoji) {
onPickEmoji(emoji);
}
},
});
export default connect(mapStateToProps, mapDispatchToProps)(EmojiPickerDropdown);

View File

@ -33,16 +33,11 @@ class GroupsCollection extends ImmutablePureComponent {
return (
<div className={[_s.default, _s.flexRow, _s.flexWrap].join(' ')}>
<ScrollableList
scrollKey='groups_collection'
emptyMessage={''}
>
{
groupIds.map((groupId, i) => (
<GroupCollectionItem key={`group-collection-item-${i}`} id={groupId} />
))
}
</ScrollableList>
</div>
)
}

View File

@ -1,169 +0,0 @@
.status-card {
display: flex;
font-size: 14px;
color: $dark-text-color;
margin-top: 14px;
text-decoration: none;
overflow: hidden;
@include border-design(lighten($ui-base-color, 8%), 1px, 4px);
&__actions {
@include flex(center, center);
@include abs-position(0, 0, 0, 0);
&>div {
background: rgba($base-shadow-color, 0.6);
border-radius: 8px;
padding: 12px 9px;
flex: 0 0 auto;
@include flex(center, center);
}
button,
a {
display: inline;
color: $secondary-text-color;
background: transparent;
border: 0;
padding: 0 8px;
text-decoration: none;
font-size: 18px;
line-height: 18px;
&:hover,
&:active,
&:focus {
color: $primary-text-color;
}
}
a {
font-size: 19px;
position: relative;
bottom: -1px;
}
}
&__title {
display: block;
font-weight: 500;
margin-bottom: 5px;
color: $darker-text-color;
text-decoration: none;
@include text-overflow(nowrap);
}
&__content {
flex: 1 1 auto;
overflow: hidden;
padding: 14px 14px 14px 8px;
}
&__description {
color: $darker-text-color;
}
&__host {
display: block;
margin-top: 5px;
font-size: 13px;
@include text-overflow(nowrap);
}
&__image {
flex: 0 0 100px;
background: lighten($ui-base-color, 8%);
position: relative;
&>.fa {
font-size: 21px;
position: absolute;
transform-origin: 50% 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
&__image-image {
border-radius: 4px 0 0 4px;
display: block;
margin: 0;
object-fit: cover;
background-size: cover;
background-position: center center;
@include size(100%);
}
&.horizontal {
display: block;
}
&.horizontal & {
&__image {
width: 100%;
}
&__image-image {
border-radius: 4px 4px 0 0;
}
&__title {
white-space: inherit;
}
}
&.compact {
border-color: lighten($ui-base-color, 4%);
&.interactive {
border: 0;
}
}
&.compact & {
&__content {
padding: 10px 8px 8px 8px;
}
&__title {
white-space: nowrap;
}
&__image {
flex: 0 0 60px;
}
}
}
a.status-card {
cursor: pointer;
&:hover {
background: lighten($ui-base-color, 8%);
}
&.compact:hover {
background-color: lighten($ui-base-color, 4%);
}
}
.status-card-photo {
cursor: zoom-in;
display: block;
text-decoration: none;
margin: 0;
@include size(100%, auto);
}
.status-card-video {
iframe {
@include size(100%);
}
}

View File

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

View File

@ -7,7 +7,7 @@ export function AccountGallery() {
}
export function Blocks() {
return import(/* webpackChunkName: "features/blocks" */'../../blocks')
return import(/* webpackChunkName: "features/blocks" */'../../blocked_accounts')
}
export function CommunityTimeline() {
@ -19,7 +19,7 @@ export function Compose() {
}
export function DomainBlocks() {
return import(/* webpackChunkName: "features/domain_blocks" */'../../domain_blocks')
return import(/* webpackChunkName: "features/domain_blocks" */'../../blocked_domains')
}
export function EmbedModal() {

View File

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

View File

@ -1,71 +0,0 @@
.video-player {
overflow: hidden;
position: relative;
background: $base-shadow-color;
max-width: 100%;
border-radius: 4px;
&:focus {
outline: 0;
}
video {
z-index: 1;
@include max-size(100vw, 80vh);
}
&.fullscreen {
margin: 0;
@include size(100% !important);
video {
@include max-size(100% !important);
@include size(100% !important);
}
}
&.inactive {
video,
.video-player__controls {
visibility: hidden;
}
}
&__spoiler {
display: none;
z-index: 4;
border: 0;
background: $base-overlay-background;
color: $darker-text-color;
transition: none;
pointer-events: none;
@include abs-position(0, auto, auto, 0);
@include size(100%);
&.active {
display: block;
pointer-events: auto;
&:hover,
&:active,
&:focus {
color: lighten($darker-text-color, 7%);
}
}
&__title {
display: block;
font-size: 14px;
}
&__subtitle {
display: block;
@include text-sizing(11px, 500);
}
}
}

View File

@ -176,7 +176,7 @@ const insertEmoji = (state, position, emojiData, needsSpace) => {
};
const privacyPreference = (a, b) => {
const order = ['public', 'unlisted', 'private', 'direct'];
const order = ['public', 'unlisted', 'private'];
return order[Math.max(order.indexOf(a), order.indexOf(b), 0)];
};

View File

@ -15,7 +15,7 @@
}
}
.emoji-mart-bar {
.emoji-mart-bar {{
border: 0 solid darken($ui-secondary-color, 8%);
&:first-child {