This commit is contained in:
mgabdev
2020-03-14 13:31:29 -04:00
parent d78dd971c0
commit 65af72faae
81 changed files with 1101 additions and 662 deletions

View File

@@ -35,6 +35,7 @@ class Account extends ImmutablePureComponent {
actionTitle: PropTypes.string,
onActionClick: PropTypes.func,
compact: PropTypes.bool,
expanded: PropTypes.bool,
showDismiss: PropTypes.bool,
dismissAction: PropTypes.func,
}
@@ -76,6 +77,7 @@ class Account extends ImmutablePureComponent {
actionIcon,
actionTitle,
compact,
expanded,
dismissAction,
showDismiss,
} = this.props

View File

@@ -35,13 +35,13 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
onBlur: PropTypes.func,
textarea: PropTypes.bool,
small: PropTypes.bool,
};
}
static defaultProps = {
autoFocus: true,
searchTokens: ['@', ':', '#'],
textarea: false,
};
}
state = {
suggestionsHidden: true,
@@ -49,11 +49,13 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0,
};
}
onChange = (e) => {
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens);
console.log('onChange', e.target.value, e.target, this.textbox, tokenStart, token)
if (token !== null && this.state.lastToken !== token) {
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
this.props.onSuggestionsFetchRequested(token);
@@ -155,10 +157,6 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
}
}
setTextbox = (c) => {
this.textbox = c;
}
renderSuggestion = (suggestion, i) => {
const { selectedSuggestion } = this.state;
let inner, key;
@@ -192,6 +190,10 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
);
}
setTextbox = (c) => {
this.textbox = c;
}
render () {
const {
value,
@@ -249,12 +251,15 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
<Fragment>
<div className={[_s.default, _s.flexGrow1].join(' ')}>
<div className={[_s.default, _s.ml5].join(' ')}>
<Textarea
className={_s.default}
inputRef={this.setTextbox}
disabled={disabled}
value={value}
aria-autocomplete='list'
/>
<ContentEditable
noFocuscontainerRefocus
ariaMultiline
contentEditable
spellcheck
tabindex='0'
ariaLabel='Gab text'
role='textbox'
@@ -264,9 +269,10 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
'white-space': 'pre-wrap',
overflowWrap: 'break-word'
}}
inputRef={this.setTextbox}
className={textClasses}
disabled={disabled}
style={style}
html={value}
placeholder={placeholder}
autoFocus={autoFocus}
onChange={this.onChange}
@@ -275,8 +281,6 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
onFocus={this.onFocus}
onBlur={this.onBlur}
onPaste={this.onPaste}
style={style}
html={value}
/>
</div>
{children}

View File

@@ -14,14 +14,21 @@ class Avatar extends ImmutablePureComponent {
}
static defaultProps = {
account: ImmutableMap(),
animate: autoPlayGif,
size: 40,
}
state = {
hovering: false,
sameImg: this.props.account.get('avatar') === this.props.account.get('avatar_static'),
sameImg: !this.props.account ? false : this.props.account.get('avatar') === this.props.account.get('avatar_static'),
}
componentDidUpdate (prevProps) {
if (prevProps.account !== this.props.account) {
this.setState({
sameImg: !this.props.account ? false : this.props.account.get('avatar') === this.props.account.get('avatar_static'),
})
}
}
handleMouseEnter = () => {
@@ -43,8 +50,8 @@ class Avatar extends ImmutablePureComponent {
className: [_s.default, _s.circle, _s.overflowHidden].join(' '),
onMouseEnter: shouldAnimate ? this.handleMouseEnter : undefined,
onMouseLeave: shouldAnimate ? this.handleMouseLeave : undefined,
src: account.get((hovering || animate) ? 'avatar' : 'avatar_static'),
alt: account.get('display_name'),
src: !account ? undefined : account.get((hovering || animate) ? 'avatar' : 'avatar_static'),
alt: !account ? undefined : account.get('display_name'),
style: {
width: `${size}px`,
height: `${size}px`,

View File

@@ -36,6 +36,7 @@ export default class Button extends PureComponent {
narrow: PropTypes.bool,
underlineOnHover: PropTypes.bool,
radiusSmall: PropTypes.bool,
noClasses: PropTypes.bool,
}
static defaultProps = {
@@ -50,6 +51,8 @@ export default class Button extends PureComponent {
}
setRef = (c) => {
const { buttonRef } = this.props
if (buttonRef) buttonRef(c)
this.node = c
}
@@ -76,6 +79,7 @@ export default class Button extends PureComponent {
underlineOnHover,
narrow,
radiusSmall,
noClasses,
...otherProps
} = this.props
@@ -89,7 +93,7 @@ export default class Button extends PureComponent {
) : undefined
// : todo :
const classes = cx(className, {
const classes = noClasses ? className : cx(className, {
default: 1,
noUnderline: 1,
font: 1,

View File

@@ -19,6 +19,7 @@ export default class Input extends PureComponent {
title: PropTypes.string,
small: PropTypes.bool,
readOnly: PropTypes.string,
inputRef: PropTypes.func,
}
render() {
@@ -34,7 +35,8 @@ export default class Input extends PureComponent {
onClear,
title,
small,
readOnly
readOnly,
inputRef
} = this.props
const inputClasses = cx({
@@ -75,6 +77,7 @@ export default class Input extends PureComponent {
className={inputClasses}
type='text'
placeholder={placeholder}
ref={inputRef}
value={value}
onChange={onChange}
onKeyUp={onKeyUp}

View File

@@ -8,10 +8,11 @@ export default class List extends PureComponent {
items: PropTypes.array,
scrollKey: PropTypes.string,
emptyMessage: PropTypes.any,
small: PropTypes.bool,
}
render() {
const { items, scrollKey, emptyMessage } = this.props
const { items, scrollKey, emptyMessage, small } = this.props
return (
<Block>
@@ -23,16 +24,16 @@ export default class List extends PureComponent {
items.map((item, i) => {
return (
<ListItem
small={small}
key={`list-item-${i}`}
to={item.to}
title={item.title}
isLast={items.length - 1 === i}
{...item}
/>
)
})
}
</ScrollableList>
</Block>
</Block>
)
}

View File

@@ -1,6 +1,7 @@
import { NavLink } from 'react-router-dom'
import classNames from 'classnames/bind'
import Button from './button'
import Icon from './icon'
import Text from './text'
const cx = classNames.bind(_s)
@@ -8,38 +9,52 @@ export default class ListItem extends PureComponent {
static propTypes = {
isLast: PropTypes.bool,
to: PropTypes.string,
href: PropTypes.string,
title: PropTypes.string,
onClick: PropTypes.func,
small: PropTypes.bool,
}
render() {
const { to, title, isLast } = this.props
const { title, isLast, to, href, onClick, small } = this.props
const containerClasses = cx({
default: 1,
cursorPointer: 1,
noUnderline: 1,
px15: 1,
py15: 1,
px15: !small,
py15: !small,
px10: small,
py10: small,
flexRow: 1,
alignItemsCenter: 1,
width100PC: 1,
backgroundSubtle_onHover: 1,
borderColorSecondary: !isLast,
borderBottom1PX: !isLast,
})
const textSize = small ? 'small' : 'normal'
return (
<NavLink to={to} className={containerClasses} >
<span className={[_s.default, _s.text, _s.colorPrimary, _s.fontSize14PX].join(' ')}>
<Button
to={to}
href={href}
onClick={onClick}
className={containerClasses}
noClasses
>
<Text color='primary' size={textSize}>
{title}
</span>
</Text>
<Icon
id='angle-right'
width='10px'
height='10px'
className={[_s.marginLeftAuto, _s.fillColorBlack].join(' ')}
/>
</NavLink>
</Button>
)
}
}

View File

@@ -1,25 +0,0 @@
import { shortNumberFormat } from '../../utils/numbers';
const mapStateToProps = state => ({
count: state.getIn(['notifications', 'unread']),
});
export default
@connect(mapStateToProps)
class NotificationCounter extends PureComponent {
static propTypes = {
count: PropTypes.number.isRequired,
};
render() {
const { count } = this.props;
if (count < 1) return null;
return (
<span className='notification-counter'>{shortNumberFormat(count)}</span>
);
}
}

View File

@@ -17,7 +17,6 @@ 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 }) => ({
@@ -55,9 +54,9 @@ class PopoverBase extends ImmutablePureComponent {
onClose: PropTypes.func.isRequired,
position: PropTypes.string,
openPopoverType: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
visible: PropTypes.bool,
targetRef: PropTypes.node,
innerRef: PropTypes.node,
}
static defaultProps = {
@@ -137,8 +136,8 @@ class PopoverBase extends ImmutablePureComponent {
disabled,
position,
openPopoverType,
openedViaKeyboard,
targetRef,
innerRef,
} = this.props
const open = this.state.id === openPopoverType
@@ -155,13 +154,10 @@ class PopoverBase extends ImmutablePureComponent {
referenceElement={targetRef}
>
{({ ref, style, placement, arrowProps }) => (
<div ref={ref} style={style} data-placement={placement}>
<div ref={ref} style={style} data-placement={placement} className={[_s.my5, _s.boxShadow2].join(' ')}>
<div ref={arrowProps.ref} style={arrowProps.style} />
<div data-popover='true' onKeyDown={this.handleKeyDown} className={containerClasses}>
{children}
{ /* <div show={open} placement={popoverPlacement} target={this.findTarget}>
<PopoverMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
</div> */}
<div ref={innerRef} data-popover='true' onKeyDown={this.handleKeyDown} className={containerClasses}>
{children}
</div>
</div>
)}

View File

@@ -19,7 +19,7 @@ const POPOVER_COMPONENTS = {
CONTENT_WARNING: () => Promise.resolve({ default: ContentWarningPopover }),
DATE_PICKER: () => Promise.resolve({ default: DatePickerPopover }),
GROUP_INFO: () => GroupInfoPopover,
PROFILE_OPTIONS: () => ProfileOptionsPopover,
PROFILE_OPTIONS: () => Promise.resolve({ default: ProfileOptionsPopover }),
SEARCH: () => Promise.resolve({ default: SearchPopover }),
SIDEBAR_MORE: () => Promise.resolve({ default: SidebarMorePopover }),
STATUS_OPTIONS: () => Promise.resolve({ default: StatusOptionsPopover }),
@@ -60,7 +60,6 @@ class PopoverRoot extends PureComponent {
onClose: PropTypes.func.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
openedViaKeyboard: PropTypes.bool,
}
static defaultProps = {
@@ -73,25 +72,23 @@ class PopoverRoot extends PureComponent {
}
handleDocumentClick = e => {
// if (this.node && !this.node.contains(e.target)) {
// this.props.onClose()
// }
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)
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 })
this.setState({ mounted: true })
}
componentWillUnmount() {
// document.removeEventListener('click', this.handleDocumentClick, false)
// document.removeEventListener('keydown', this.handleKeyDown, false)
// document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions)
document.removeEventListener('click', this.handleDocumentClick, false)
document.removeEventListener('keydown', this.handleKeyDown, false)
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions)
}
setRef = c => {
@@ -165,12 +162,10 @@ class PopoverRoot extends PureComponent {
const { mounted } = this.state
const visible = !!type
console.log("popover root - type, visible:", type, visible, props, POPOVER_COMPONENTS[type])
return (
<PopoverBase
visible={visible}
ref={this.setRef}
innerRef={this.setRef}
{...props}
>
{

View File

@@ -1,11 +1,245 @@
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'
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, autoPlayGif, me, isStaff } from '../../initial_state'
import { makeGetAccount } from '../../selectors'
import PopoverLayout from './popover_layout'
import Text from '../text'
import List from '../list'
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
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' },
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { account }) => ({
account: getAccount(state, !!account ? account.get('id') : -1),
domain: state.getIn(['meta', 'domain']),
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (unfollowModal) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(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('CONFIRM', {
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.get('id'))),
secondary: intl.formatMessage(messages.blockAndReport),
onSecondary: () => {
dispatch(blockAccount(account.get('id')));
dispatch(initReport(account));
},
}));
}
},
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('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
}));
},
onUnblockDomain (domain) {
dispatch(unblockDomain(domain));
},
onAddToList(account){
dispatch(openModal('LIST_ADDER', {
accountId: account.get('id'),
}));
},
});
export default
@injectIntl
@connect(makeMapStateToProps, mapDispatchToProps)
class ProfileOptionsPopover extends PureComponent {
makeMenu() {
const { account, intl, domain } = this.props;
let menu = [];
if (!account) {
return [];
}
if ('share' in navigator) {
menu.push({ title: intl.formatMessage(messages.share, { name: account.get('username') }), onClick: this.handleShare });
}
if (account.get('id') === me) {
menu.push({ title: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
menu.push({ title: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
menu.push({ title: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
menu.push({ title: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ title: intl.formatMessage(messages.blocks), to: '/blocks' });
menu.push({ title: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
} else {
menu.push({ title: intl.formatMessage(messages.mention, { name: account.get('acct') }), onClick: this.props.onMention });
if (account.getIn(['relationship', 'following'])) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
menu.push({ title: intl.formatMessage(messages.hideReposts, { name: account.get('username') }), onClick: this.props.onRepostToggle });
} else {
menu.push({ title: intl.formatMessage(messages.showReposts, { name: account.get('username') }), onClick: this.props.onRepostToggle });
}
menu.push({ title: intl.formatMessage(messages.add_or_remove_from_list), onClick: this.props.onAddToList });
menu.push({ title: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), onClick: this.props.onEndorseToggle });
}
if (account.getIn(['relationship', 'muting'])) {
menu.push({ title: intl.formatMessage(messages.unmute, { name: account.get('username') }), onClick: this.props.onMute });
} else {
menu.push({ title: intl.formatMessage(messages.mute, { name: account.get('username') }), onClick: this.props.onMute });
}
if (account.getIn(['relationship', 'blocking'])) {
menu.push({ title: intl.formatMessage(messages.unblock, { name: account.get('username') }), onClick: this.props.onBlock });
} else {
menu.push({ title: intl.formatMessage(messages.block, { name: account.get('username') }), onClick: this.props.onBlock });
}
menu.push({ title: intl.formatMessage(messages.report, { name: account.get('username') }), onClick: this.props.onReport });
}
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
if (account.getIn(['relationship', 'domain_blocking'])) {
menu.push({ title: intl.formatMessage(messages.unblockDomain, { domain }), onClick: this.props.onUnblockDomain });
} else {
menu.push({ title: intl.formatMessage(messages.blockDomain, { domain }), onClick: this.props.onBlockDomain });
}
}
if (account.get('id') !== me && isStaff) {
menu.push({ title: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
}
return menu;
}
export default class UserInfoPopover extends PureComponent {
render() {
const listItems = this.makeMenu()
return (
<PopoverLayout>
<Text>testing</Text>
<List
scrollKey='profile_options'
items={listItems}
small
/>
</PopoverLayout>
)
}

View File

@@ -1,9 +1,29 @@
import PopoverLayout from './popover_layout'
import List from '../list'
export default class SidebarMorePopover extends PureComponent {
render() {
return (
<div>
{ /* */ }
</div>
<PopoverLayout>
<List
scrollKey='profile_options'
items={[
{
title: 'Help',
href: 'https://help.gab.com',
},
{
title: 'Settings',
href: '/settings',
},
{
title: 'Log Out',
href: '/auth/log_out',
},
]}
small
/>
</PopoverLayout>
)
}
}

View File

@@ -0,0 +1,278 @@
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'
import classNames from 'classnames/bind'
import {
followAccount,
unfollowAccount,
blockAccount,
unblockAccount,
} from '../actions/accounts'
import { openPopover, closePopover } from '../actions/popover'
import { initReport } from '../actions/reports'
import { openModal } from '../actions/modal'
import { unfollowModal } from '../initial_state'
import Avatar from './avatar'
import Image from './image'
import Text from './text'
import Button from './button'
import DisplayName from './display_name'
import TabBar from './tab_bar'
const cx = classNames.bind(_s)
const messages = defineMessages({
followers: { id: 'account.followers', defaultMessage: 'Followers' },
follows: { id: 'account.follows', defaultMessage: 'Follows' },
profile: { id: 'account.profile', defaultMessage: 'Profile' },
})
const mapStateToProps = state => {
return {
}
}
const mapDispatchToProps = (dispatch, { intl }) => ({
openProfileOptionsPopover(props) {
console.log("props:", props)
dispatch(openPopover('PROFILE_OPTIONS', props))
},
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (unfollowModal) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(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('CONFIRM', {
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.blockConfirm),
onConfirm: () => dispatch(blockAccount(account.get('id'))),
secondary: intl.formatMessage(messages.blockAndReport),
onSecondary: () => {
dispatch(blockAccount(account.get('id')));
dispatch(initReport(account));
},
}));
}
},
onRepostToggle (account) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
dispatch(followAccount(account.get('id'), false));
} else {
dispatch(followAccount(account.get('id'), true));
}
},
});
export default
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class ProfileHeader extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
intl: PropTypes.object.isRequired,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
openProfileOptionsPopover: PropTypes.func.isRequired,
}
handleOpenMore = () => {
const { openProfileOptionsPopover, account } = this.props
openProfileOptionsPopover({
targetRef: this.openMoreNode,
position: 'top',
account: this.props.account,
})
}
handleStartChat = () => {
}
handleFollow = () => {
}
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
}
setOpenMoreNodeRef = (n) => {
this.openMoreNode = n
}
render() {
const { account, intl } = this.props
const tabs = !account ? null : [
{
to: `/${account.get('acct')}`,
title: 'Timeline',
},
{
to: `/${account.get('acct')}/comments`,
title: 'Comments',
},
{
to: `/${account.get('acct')}/media`,
title: 'Media',
},
{
to: '',
title: 'More',
},
]
const headerSrc = !!account ? account.get('header') : ''
const headerMissing = headerSrc.indexOf('/headers/original/missing.png') > -1 || !headerSrc
const avatarContainerClasses = cx({
circle: 1,
marginTopNeg75PX: !headerMissing,
borderColorWhite: 1,
border2PX: 1,
})
const avatarSize = headerMissing ? '75' : '150'
return (
<div className={[_s.default, _s.z1, _s.width100PC].join(' ')}>
{
!headerMissing &&
<div className={[_s.default, _s.height350PX, _s.width100PC, _s.radiusSmall, _s.overflowHidden].join(' ')}>
<Image
className={_s.height350PX}
src={headerSrc}
/>
</div>
}
<div className={[_s.default, _s.borderBottom1PX, _s.borderColorSecondary, _s.width100PC].join(' ')}>
<div className={[_s.default, _s.flexRow, _s.px15].join(' ')}>
<div className={avatarContainerClasses}>
<Avatar size={avatarSize} account={account} />
</div>
<div className={[_s.default, _s.px15, _s.py10].join(' ')}>
<DisplayName account={account} multiline large />
</div>
</div>
<div className={[_s.default, _s.flexRow, _s.borderBottom1PX, _s.borderColorSecondary, _s.mt5, _s.height53PX].join(' ')}>
<div className={[_s.default].join(' ')}>
<TabBar tabs={tabs} large />
</div>
<div className={[_s.default, _s.flexRow, _s.marginLeftAuto, _s.py5].join(' ')}>
<div ref={this.setOpenMoreNodeRef}>
<Button
outline
icon='ellipsis'
iconWidth='18px'
iconHeight='18px'
iconClassName={_s.fillColorBrand}
color='brand'
backgroundColor='none'
className={[_s.justifyContentCenter, _s.alignItemsCenter, _s.mr10, _s.px10].join(' ')}
onClick={this.handleOpenMore}
/>
</div>
<Button
outline
icon='chat'
iconWidth='18px'
iconHeight='18px'
iconClassName={_s.fillColorBrand}
color='brand'
backgroundColor='none'
className={[_s.justifyContentCenter, _s.alignItemsCenter, _s.mr10, _s.px10].join(' ')}
onClick={this.handleStartChat}
/>
<Button
className={[_s.justifyContentCenter, _s.alignItemsCenter].join(' ')}
onClick={this.handleFollow}
>
<span className={[_s.px15].join(' ')}>
<Text
color='white'
weight='bold'
size='medium'
className={[_s.px15].join(' ')}
>
Follow
</Text>
</span>
</Button>
</div>
</div>
</div>
</div>
)
}
}

View File

@@ -43,18 +43,19 @@ class Search extends PureComponent {
value: PropTypes.string.isRequired,
submitted: PropTypes.bool,
onShow: PropTypes.func.isRequired,
openInRoute: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onKeyUp: PropTypes.func.isRequired,
handleSubmit: PropTypes.func,
withOverlay: PropTypes.bool,
handleClear: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
}
state = {
expanded: false,
}
textbox = React.createRef()
handleChange = (e) => {
this.props.onChange(e.target.value)
}
@@ -68,8 +69,31 @@ class Search extends PureComponent {
this.setState({ expanded: false })
}
handleKeyUp = (e) => {
const { value } = this.props
if (e.key === 'Enter') {
e.preventDefault();
this.props.onSubmit();
this.context.router.history.push(`/search?q=${value}`);
} else if (e.key === 'Escape') {
this.textbox.blur()
}
}
setTextbox = n => {
this.textbox = n
}
render() {
const { value, submitted, onKeyUp, handleClear, handleSubmit, withOverlay } = this.props
const {
value,
submitted,
handleClear,
withOverlay
} = this.props
const { expanded } = this.state
const hasValue = value ? value.length > 0 || submitted : 0
@@ -79,10 +103,11 @@ class Search extends PureComponent {
<Input
hasClear
value={value}
inputRef={this.setTextbox}
prependIcon='search'
placeholder='Search on Gab...'
onChange={this.handleChange}
onKeyUp={onKeyUp}
onKeyUp={this.handleKeyUp}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onClear={handleClear}

View File

@@ -4,6 +4,7 @@ import { injectIntl, defineMessages } from 'react-intl'
import Button from './button'
import { closeSidebar } from '../actions/sidebar'
import { openModal } from '../actions/modal'
import { openPopover } from '../actions/popover'
import { me } from '../initial_state'
import { makeGetAccount } from '../selectors'
import SidebarSectionTitle from './sidebar_section_title'
@@ -37,6 +38,8 @@ const mapStateToProps = state => {
return {
account: getAccount(state, me),
sidebarOpen: state.get('sidebar').sidebarOpen,
notificationCount: state.getIn(['notifications', 'unread']),
homeItemsQueueCount: state.getIn(['timelines', 'home', 'totalQueuedItemsCount']),
}
}
@@ -44,6 +47,9 @@ const mapDispatchToProps = (dispatch) => ({
onClose() {
dispatch(closeSidebar())
},
openSidebarMorePopover(props) {
dispatch(openPopover('SIDEBAR_MORE', props))
},
onOpenComposeModal() {
dispatch(openModal('COMPOSE'))
},
@@ -60,6 +66,9 @@ class Sidebar extends ImmutablePureComponent {
sidebarOpen: PropTypes.bool,
onClose: PropTypes.func.isRequired,
onOpenComposeModal: PropTypes.func.isRequired,
openSidebarMorePopover: PropTypes.func.isRequired,
notificationCount: PropTypes.number.isRequired,
homeItemsQueueCount: PropTypes.number.isRequired,
}
state = {
@@ -90,12 +99,23 @@ class Sidebar extends ImmutablePureComponent {
}
handleOpenComposeModal = () => {
console.log("handleOpenComposeModal")
this.props.onOpenComposeModal()
}
handleOpenSidebarMorePopover =() => {
console.log("handleOpenSidebarMorePopover")
this.props.openSidebarMorePopover({
targetRef: this.moreBtnRef,
position: 'top',
})
}
setMoreButtonRef = n => {
this.moreBtnRef = n
}
render() {
const { sidebarOpen, intl, account } = this.props
const { sidebarOpen, intl, account, notificationCount, homeItemsQueueCount } = this.props
const { moreOpen } = this.state
// : todo :
@@ -112,19 +132,20 @@ class Sidebar extends ImmutablePureComponent {
title: 'Home',
icon: 'home',
to: '/',
count: 124,
count: homeItemsQueueCount,
},
{
title: 'Notifications',
icon: 'notifications',
to: '/notifications',
count: 40,
},
{
title: 'Search',
icon: 'search-alt',
to: '/search',
count: notificationCount,
},
// : todo : show only when search on top is not visible
// {
// title: 'Search',
// icon: 'search-alt',
// to: '/search',
// },
{
title: 'Groups',
icon: 'group',
@@ -138,17 +159,17 @@ class Sidebar extends ImmutablePureComponent {
{
title: 'Chat',
icon: 'chat',
to: '',
// href: 'https://chat.gab.com',
href: 'https://chat.gab.com',
},
{
title: 'More',
icon: 'more',
to: '/',
onClick: this.handleOpenSidebarMorePopover,
buttonRef: this.setMoreButtonRef
},
]
// more:
// more modal:
// settings/preferences
// help
// logout
@@ -172,26 +193,22 @@ class Sidebar extends ImmutablePureComponent {
{
title: 'Apps',
icon: 'apps',
to: '',
// href: 'https://apps.gab.com',
href: 'https://apps.gab.com',
},
{
title: 'Shop',
icon: 'shop',
to: '',
// href: 'https://shop.dissenter.com',
href: 'https://shop.dissenter.com',
},
{
title: 'Trends',
icon: 'trends',
to: '',
// href: 'https://trends.gab.com',
href: 'https://trends.gab.com',
},
{
title: 'Dissenter',
icon: 'dissenter',
to: '',
// href: 'https://dissenter.com',
href: 'https://dissenter.com',
},
]
@@ -199,7 +216,7 @@ class Sidebar extends ImmutablePureComponent {
<header role='banner' className={[_s.default, _s.flexGrow1, _s.z3, _s.alignItemsEnd].join(' ')}>
<div className={[_s.default, _s.width240PX].join(' ')}>
<div className={[_s.default, _s.positionFixed, _s.top0, _s.height100PC].join(' ')}>
<div className={[_s.default, _s.height100PC, _s.width240PX, _s.pr15, _s.my10].join(' ')}>
<div className={[_s.default, _s.height100PC, _s.width240PX, _s.pr15, _s.py10, _s.overflowYScroll].join(' ')}>
<SidebarHeader />

View File

@@ -1,20 +1,21 @@
import { NavLink } from 'react-router-dom'
import classNames from 'classnames/bind'
import Button from './button'
import Icon from './icon'
import Text from './text'
const cx = classNames.bind(_s)
export default class SidebarSectionItem extends PureComponent {
static propTypes = {
to: PropTypes.string,
href: PropTypes.string,
onClick: PropTypes.func,
active: PropTypes.bool,
icon: PropTypes.string,
image: PropTypes.string,
title: PropTypes.string,
me: PropTypes.bool,
suffix: PropTypes.node,
buttonRef: PropTypes.func,
}
state = {
@@ -30,7 +31,18 @@ export default class SidebarSectionItem extends PureComponent {
}
render() {
const { to, active, icon, image, title, me, count } = this.props
const {
to,
active,
icon,
image,
title,
me,
count,
onClick,
href,
buttonRef
} = this.props
const { hovering } = this.state
const iconSize = '16px'
@@ -49,6 +61,7 @@ export default class SidebarSectionItem extends PureComponent {
// border1PX: shouldShowActive,
// borderColorSecondary: shouldShowActive,
backgroundSubtle2: shouldShowActive,
backgroundTransparent: 1,
})
const textClasses = cx({
@@ -63,7 +76,7 @@ export default class SidebarSectionItem extends PureComponent {
const iconClasses = cx({
fillColorBlack: shouldShowActive,
fillcolorSecondary: !hovering && !active,
fillColorSecondary: !hovering && !active,
})
const countClasses = cx({
@@ -82,11 +95,15 @@ export default class SidebarSectionItem extends PureComponent {
})
return (
<NavLink
<Button
to={to}
href={href}
onClick={onClick}
noClasses
buttonRef={buttonRef}
onMouseEnter={() => this.handleOnMouseEnter()}
onMouseLeave={() => this.handleOnMouseLeave()}
className={[_s.default, _s.noUnderline, _s.cursorPointer, _s.width100PC, _s.alignItemsStart].join(' ')}
className={[_s.default, _s.noUnderline, _s.cursorPointer, _s.width100PC, _s.alignItemsStart, _s.backgroundTransparent].join(' ')}
>
<div className={containerClasses}>
<div className={[_s.default]}>
@@ -109,7 +126,7 @@ export default class SidebarSectionItem extends PureComponent {
</span>
}
</div>
</NavLink>
</Button>
)
}
}

View File

@@ -348,7 +348,7 @@ class Status extends ImmutablePureComponent {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'>
<Icon id='retweet' className='status__prepend-icon' fixedWidth />
<Icon id='repost' className='status__prepend-icon' fixedWidth />
</div>
{/*<FormattedMessage
id='status.reposted_by'
@@ -382,7 +382,7 @@ class Status extends ImmutablePureComponent {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const video = status.getIn(['media_attachments', 0]);
console.log("VIDEO HERE")
// console.log("VIDEO HERE")
media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer}>
@@ -435,6 +435,10 @@ class Status extends ImmutablePureComponent {
)
}
// console.log("da status:", status)
let quotedStatus = status.get('quotedStatus');
// console.log("quotedStatus:", quotedStatus)
const handlers = this.props.muted ? {} : {
reply: this.handleHotkeyReply,
favorite: this.handleHotkeyFavorite,
@@ -509,9 +513,9 @@ class Status extends ImmutablePureComponent {
<StatusActionBar status={status} account={account} {...other} />
{ /* <div className={[_s.default, _s.borderTop1PX, _s.borderColorSecondary, _s.pt10, _s.px15, _s.mb10].join(' ')}>
<ComposeFormContainer statusId={status.get('id')} shouldCondense />
</div> */ }
<div className={[_s.default, _s.borderTop1PX, _s.borderColorSecondary, _s.pt10, _s.px15, _s.mb10].join(' ')}>
{/*<ComposeFormContainer replyToId={status.get('id')} shouldCondense />*/}
</div>
</div>
</div>
</div>

View File

@@ -27,7 +27,7 @@ class StatusContent extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
reblogContent: PropTypes.string,
reblogStatus: PropTypes.string,
expanded: PropTypes.bool,
onExpandedToggle: PropTypes.func,
onClick: PropTypes.func,

View File

@@ -1,7 +1,7 @@
import Toggle from 'react-toggle';
import Toggle from 'react-toggle'
export default class ToggleSwitch extends PureComponent {
render() {
return <Toggle {...this.props} />
};
}
}