Progress
This commit is contained in:
@@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import isObject from 'lodash.isobject'
|
||||
import classNames from 'classnames/bind'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import Textarea from 'react-textarea-autosize'
|
||||
import { isRtl } from '../utils/rtl'
|
||||
import { textAtCursorMatchesToken } from '../utils/cursor_token_match'
|
||||
import AutosuggestAccount from './autosuggest_account'
|
||||
@@ -226,27 +227,56 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
|
||||
direction: isRtl(value) ? 'rtl' : 'ltr',
|
||||
}
|
||||
|
||||
const textClasses = cx({
|
||||
const textareaClasses = cx({
|
||||
default: 1,
|
||||
lineHeight125: 1,
|
||||
font: 1,
|
||||
wrap: 1,
|
||||
resizeNone: 1,
|
||||
text: 1,
|
||||
displayBlock: 1,
|
||||
bgTransparent: 1,
|
||||
outlineNone: 1,
|
||||
bgPrimary: !small,
|
||||
bgSubtle: small,
|
||||
py15: !small,
|
||||
py10: small,
|
||||
lineHeight125: 1,
|
||||
height100PC: small,
|
||||
width100PC: !small,
|
||||
pt15: !small,
|
||||
px15: !small,
|
||||
px10: small,
|
||||
pb10: !small,
|
||||
fs16PX: !small,
|
||||
fs14PX: small,
|
||||
mr5: small,
|
||||
heightMax200PX: small,
|
||||
heightMax80VH: !small,
|
||||
heightMin100PX: !small,
|
||||
})
|
||||
|
||||
const textareaContainerClasses = cx({
|
||||
default: 1,
|
||||
maxWidth100PC: 1,
|
||||
flexGrow1: small,
|
||||
height100PC: small,
|
||||
justifyContentCenter: small,
|
||||
})
|
||||
|
||||
if (textarea) {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={[_s.default, _s.flexGrow1, _s.maxWidth100PC].join(' ')}>
|
||||
<Composer
|
||||
<div className={textareaContainerClasses}>
|
||||
<Textarea
|
||||
inputRef={this.setTextbox}
|
||||
className={textareaClasses}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={false}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
// onKeyDown={this.onKeyDown}
|
||||
// onKeyUp={onKeyUp}
|
||||
// onFocus={this.onFocus}
|
||||
// onBlur={this.onBlur}
|
||||
// onPaste={this.onPaste}
|
||||
aria-autocomplete='list'
|
||||
/>
|
||||
|
||||
{/*<Composer
|
||||
inputRef={this.setTextbox}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
@@ -259,7 +289,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
|
||||
onBlur={this.onBlur}
|
||||
onPaste={this.onPaste}
|
||||
small={small}
|
||||
/>
|
||||
/>*/}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,52 +1,125 @@
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import { makeGetStatus } from '../selectors';
|
||||
import {
|
||||
favorite,
|
||||
unfavorite,
|
||||
} from '../actions/interactions'
|
||||
import { replyCompose } from '../actions/compose'
|
||||
import { openModal } from '../actions/modal'
|
||||
import { openPopover } from '../actions/popover'
|
||||
import { makeGetStatus } from '../selectors'
|
||||
import { me } from '../initial_state'
|
||||
import Avatar from './avatar'
|
||||
import Button from './button'
|
||||
import CommentHeader from './comment_header'
|
||||
import StatusContent from './status_content'
|
||||
import StatusMedia from './status_media'
|
||||
import { defaultMediaVisibility } from './status'
|
||||
import Text from './text'
|
||||
|
||||
const messages = defineMessages({
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
like: { id: 'status.like', defaultMessage: 'Like' },
|
||||
unlike: { id: 'status.unlike', defaultMessage: 'Unlike' },
|
||||
})
|
||||
|
||||
const makeMapStateToProps = (state, props) => ({
|
||||
status: makeGetStatus()(state, props)
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onReply (status, router) {
|
||||
if (!me) return dispatch(openModal('UNAUTHORIZED'))
|
||||
|
||||
dispatch((_, getState) => {
|
||||
const state = getState();
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.reply.message' defaultMessage='Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' />,
|
||||
confirm: <FormattedMessage id='confirmations.reply.confirm' defaultMessage='Reply' />,
|
||||
onConfirm: () => dispatch(replyCompose(status, router)),
|
||||
}))
|
||||
} else {
|
||||
dispatch(replyCompose(status, router, true))
|
||||
}
|
||||
})
|
||||
},
|
||||
onFavorite (status) {
|
||||
if (!me) return dispatch(openModal('UNAUTHORIZED'))
|
||||
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavorite(status))
|
||||
} else {
|
||||
dispatch(favorite(status))
|
||||
}
|
||||
},
|
||||
onOpenStatusOptions(status) {
|
||||
dispatch(openPopover('STATUS_OPTOINS', { status }))
|
||||
},
|
||||
})
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
@connect(makeMapStateToProps)
|
||||
@connect(makeMapStateToProps, mapDispatchToProps)
|
||||
class Comment extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
indent: PropTypes.number,
|
||||
intl: PropTypes.object.isRequired,
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
isHidden: PropTypes.bool,
|
||||
isIntersecting: PropTypes.bool,
|
||||
onReply: PropTypes.func.isRequired,
|
||||
onFavorite: PropTypes.func.isRequired,
|
||||
onOpenStatusOptions: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
updateOnProps = [
|
||||
'status',
|
||||
'indent',
|
||||
'isHidden',
|
||||
'isIntersecting',
|
||||
]
|
||||
|
||||
state = {
|
||||
showMedia: defaultMediaVisibility(this.props.status),
|
||||
}
|
||||
|
||||
handleOnReply = () => {
|
||||
this.props.onReply(this.props.status)
|
||||
}
|
||||
|
||||
handleOnFavorite = () => {
|
||||
this.props.onFavorite(this.props.status)
|
||||
}
|
||||
|
||||
handleOnOpenStatusOptions = () => {
|
||||
this.props.onOpenStatusOptions(this.props.status)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
indent,
|
||||
intl,
|
||||
status,
|
||||
isHidden,
|
||||
} = this.props
|
||||
|
||||
if (isHidden) {
|
||||
return (
|
||||
<div tabIndex='0'>
|
||||
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
|
||||
{status.get('content')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const style = {
|
||||
paddingLeft: `${indent * 40}px`,
|
||||
}
|
||||
|
||||
// : todo : add media
|
||||
|
||||
return (
|
||||
<div className={[_s.default, _s.px15, _s.mb10, _s.py5].join(' ')} data-comment={status.get('id')}>
|
||||
<div className={[_s.default].join(' ')} style={style}>
|
||||
@@ -69,12 +142,34 @@ class Comment extends ImmutablePureComponent {
|
||||
isComment
|
||||
collapsable
|
||||
/>
|
||||
<div className={[_s.default].join(' ')}>
|
||||
<StatusMedia
|
||||
isComment
|
||||
status={status}
|
||||
onOpenMedia={this.props.onOpenMedia}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
width={this.props.cachedMediaWidth}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={[_s.default, _s.flexRow, _s.mt5].join(' ')}>
|
||||
<CommentButton title={intl.formatMessage(messages.like)} />
|
||||
<CommentButton title={intl.formatMessage(messages.reply)} />
|
||||
<CommentButton title='···' />
|
||||
<CommentButton
|
||||
title={intl.formatMessage(status.get('favourited') ? messages.unlike: messages.like)}
|
||||
onClick={this.handleOnFavorite}
|
||||
/>
|
||||
<CommentButton
|
||||
title={intl.formatMessage(messages.reply)}
|
||||
onClick={this.handleOnReply}
|
||||
/>
|
||||
<CommentButton
|
||||
title='···'
|
||||
onClick={this.handleOnOpenStatusOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import Button from './button'
|
||||
import Comment from './comment'
|
||||
import ScrollableList from './scrollable_list'
|
||||
import Text from './text'
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
export default class CommentList extends ImmutablePureComponent {
|
||||
|
||||
@@ -11,34 +13,44 @@ export default class CommentList extends ImmutablePureComponent {
|
||||
descendants: ImmutablePropTypes.list,
|
||||
}
|
||||
|
||||
handleLoadMore = () => {
|
||||
//
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
descendants,
|
||||
commentsLimited,
|
||||
} = this.props
|
||||
|
||||
const upperLimit = 6
|
||||
const size = descendants.size
|
||||
const max = Math.min(commentsLimited ? 2 : 6, size)
|
||||
console.log("max:", size, max)
|
||||
const max = Math.min(commentsLimited ? 2 : upperLimit, size)
|
||||
|
||||
const Wrapper = !commentsLimited ? ScrollableList : DummyContainer
|
||||
console.log("Wrapper:", Wrapper)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Wrapper scrollKey='comments'>
|
||||
{
|
||||
descendants.slice(0, max).map((descendant, i) => (
|
||||
<Comment
|
||||
key={`comment-${descendant.get('statusId')}-${i}`}
|
||||
id={descendant.get('statusId')}
|
||||
indent={descendant.get('indent')}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Wrapper>
|
||||
{
|
||||
descendants.slice(0, max).map((descendant, i) => (
|
||||
<Comment
|
||||
key={`comment-${descendant.get('statusId')}-${i}`}
|
||||
id={descendant.get('statusId')}
|
||||
indent={descendant.get('indent')}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
size > 0 && size > max &&
|
||||
size > 0 && size > max && commentsLimited &&
|
||||
<div className={[_s.default, _s.flexRow, _s.px15, _s.pb5, _s.mb10, _s.alignItemsCenter].join(' ')}>
|
||||
<Button
|
||||
isText
|
||||
backgroundColor='none'
|
||||
color='tertiary'
|
||||
onClick={this.handleLoadMore}
|
||||
>
|
||||
<Text weight='bold' color='inherit'>
|
||||
View more comments
|
||||
@@ -58,3 +70,9 @@ export default class CommentList extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DummyContainer extends PureComponent {
|
||||
render() {
|
||||
return <div>{this.props.children}</div>
|
||||
}
|
||||
}
|
||||
@@ -100,8 +100,22 @@ class Composer extends PureComponent {
|
||||
}
|
||||
|
||||
state = {
|
||||
markdownText: '',
|
||||
plainText: '',
|
||||
editorState: EditorState.createEmpty(compositeDecorator),
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
// if (!nextProps.isHidden && nextProps.isIntersecting && !prevState.fetched) {
|
||||
// return {
|
||||
// fetched: true
|
||||
// }
|
||||
// }
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// EditorState.createWithContent(ContentState.createFromText('Hello'))
|
||||
|
||||
onChange = (editorState) => {
|
||||
this.setState({ editorState })
|
||||
@@ -123,15 +137,14 @@ class Composer extends PureComponent {
|
||||
this.props.onChange(null, text, selectionStart, markdownString)
|
||||
}
|
||||
|
||||
// **bold**
|
||||
// *italic*
|
||||
// __underline__
|
||||
// ~strikethrough~
|
||||
// # title
|
||||
// > quote
|
||||
// `code`
|
||||
// ```code```
|
||||
|
||||
// **bold**
|
||||
// *italic*
|
||||
// __underline__
|
||||
// ~strikethrough~
|
||||
// # title
|
||||
// > quote
|
||||
// `code`
|
||||
// ```code```
|
||||
|
||||
focus = () => {
|
||||
this.textbox.editor.focus()
|
||||
@@ -168,7 +181,7 @@ class Composer extends PureComponent {
|
||||
disabled,
|
||||
placeholder,
|
||||
autoFocus,
|
||||
// value,
|
||||
value,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onKeyUp,
|
||||
|
||||
@@ -122,7 +122,7 @@ class DisplayName extends ImmutablePureComponent {
|
||||
|
||||
const iconSize =
|
||||
!!isLarge ? '19px' :
|
||||
!!isSmall ? '14px' : '16px'
|
||||
!!isSmall ? '14px' : '15px'
|
||||
|
||||
const domain = account.get('acct').split('@')[1]
|
||||
const isRemoteUser = !!domain
|
||||
|
||||
@@ -29,7 +29,7 @@ const emojify = (str, customEmojis = {}) => {
|
||||
// if you want additional emoji handler, add statements below which set replacement and return true.
|
||||
if (shortname in customEmojis) {
|
||||
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
|
||||
replacement = `<img draggable="false" style="height:20px;width:20px;margin:-3px 0 0;" alt="${shortname}" title="${shortname}" src="${filename}" />`;
|
||||
replacement = `<img draggable="false" style="height:16px;width:16px;margin:-3px 0 0;font-family: 'object-fit:contain',inherit;vertical-align: middle;-o-object-fit: contain;object-fit: contain;" alt="${shortname}" title="${shortname}" src="${filename}" />`;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -60,7 +60,7 @@ const emojify = (str, customEmojis = {}) => {
|
||||
} else { // matched to unicode emoji
|
||||
const { filename, shortCode } = unicodeMapping[match];
|
||||
const title = shortCode ? `:${shortCode}:` : '';
|
||||
replacement = `<img draggable="false" style="height:20px;width:20px;margin: -3px 0 0;" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`;
|
||||
replacement = `<img draggable="false" style="height:16px;width:16px;margin:-3px 0 0;font-family: 'object-fit:contain',inherit;vertical-align: middle;-o-object-fit: contain;object-fit: contain;" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`;
|
||||
rend = i + match.length;
|
||||
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
|
||||
if (str.codePointAt(rend) === 65038) {
|
||||
|
||||
@@ -9,48 +9,39 @@ export default class ExtendedVideoPlayer extends PureComponent {
|
||||
controls: PropTypes.bool.isRequired,
|
||||
muted: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
}
|
||||
|
||||
handleLoadedData = () => {
|
||||
if (this.props.time) {
|
||||
this.video.currentTime = this.props.time;
|
||||
this.video.currentTime = this.props.time
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||
this.video.addEventListener('loadeddata', this.handleLoadedData)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.video.removeEventListener('loadeddata', this.handleLoadedData);
|
||||
this.video.removeEventListener('loadeddata', this.handleLoadedData)
|
||||
}
|
||||
|
||||
setRef = (c) => {
|
||||
this.video = c;
|
||||
this.video = c
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
e.stopPropagation();
|
||||
const handler = this.props.onClick;
|
||||
if (handler) handler();
|
||||
e.stopPropagation()
|
||||
const handler = this.props.onClick
|
||||
if (handler) handler()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { src, muted, controls, alt } = this.props;
|
||||
|
||||
// .extended-video-player {
|
||||
// @include size(100%);
|
||||
// @include flex(center, center);
|
||||
|
||||
// video {
|
||||
// @include max-size($media-modal-media-max-width, $media-modal-media-max-height);
|
||||
// }
|
||||
// }
|
||||
|
||||
const { src, muted, controls, alt } = this.props
|
||||
|
||||
return (
|
||||
<div className='extended-video-player'>
|
||||
<div className={[_s.default, _s.width100PC, _s.height100PC, _s.alignItemsCenter, _s.justifyContentCenter].join(' ')}>
|
||||
<video
|
||||
className={[_s.default, _s.maxWidth100PC, _s.heightMax100PC].join(' ')}
|
||||
playsInline
|
||||
ref={this.setRef}
|
||||
src={src}
|
||||
@@ -64,7 +55,7 @@ export default class ExtendedVideoPlayer extends PureComponent {
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const messages = defineMessages({
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
toggleMembership(group, relationships) {
|
||||
onToggleMembership(group, relationships) {
|
||||
if (relationships.get('member')) {
|
||||
dispatch(leaveGroup(group.get('id')));
|
||||
} else {
|
||||
@@ -116,6 +116,7 @@ class GroupHeader extends ImmutablePureComponent {
|
||||
<Button
|
||||
radiusSmall
|
||||
className={_s.mr5}
|
||||
onClick={this.handleOnToggleMembership}
|
||||
{...actionButtonOptions}
|
||||
>
|
||||
<Text color='inherit' size='small'>
|
||||
|
||||
@@ -24,6 +24,7 @@ export default class Input extends PureComponent {
|
||||
inputRef: PropTypes.func,
|
||||
id: PropTypes.string.isRequired,
|
||||
hideLabel: PropTypes.bool,
|
||||
maxLength: PropTypes.number,
|
||||
}
|
||||
|
||||
handleOnChange = (e) => {
|
||||
@@ -46,7 +47,8 @@ export default class Input extends PureComponent {
|
||||
readOnly,
|
||||
inputRef,
|
||||
id,
|
||||
hideLabel
|
||||
hideLabel,
|
||||
maxLength,
|
||||
} = this.props
|
||||
|
||||
const inputClasses = cx({
|
||||
@@ -87,7 +89,7 @@ export default class Input extends PureComponent {
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
<div className={[_s.default, _s.bgPrimary, _s.border1PX, _s.borderColorSecondary, _s.flexRow, _s.circle, _s.alignItemsCenter].join(' ')}>
|
||||
<div className={[_s.default, _s.flexGrow1, _s.bgPrimary, _s.border1PX, _s.borderColorSecondary, _s.flexRow, _s.circle, _s.alignItemsCenter].join(' ')}>
|
||||
{
|
||||
!!prependIcon &&
|
||||
<Icon id={prependIcon} size='16px' className={[_s.fillPrimary, _s.ml15, _s.mr5].join(' ')} />
|
||||
@@ -110,6 +112,7 @@ export default class Input extends PureComponent {
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
readOnly={readOnly}
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ class LoadMore extends PureComponent {
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
this.props.onClick()
|
||||
this.props.onClick(e)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -309,10 +309,11 @@ class MediaGallery extends PureComponent {
|
||||
} = this.props
|
||||
const { visible } = this.state
|
||||
|
||||
const width = this.state.width || defaultWidth;
|
||||
let width = this.state.width || defaultWidth
|
||||
if (reduced) width = width / 2
|
||||
|
||||
const style = {};
|
||||
const size = media.take(4).size;
|
||||
const style = {}
|
||||
const size = media.take(4).size
|
||||
|
||||
const standard169 = width / (16 / 9);
|
||||
const standard169_percent = 100 / (16 / 9);
|
||||
@@ -517,9 +518,9 @@ class MediaGallery extends PureComponent {
|
||||
|
||||
//If reduced (i.e. like in a quoted post)
|
||||
//then we need to make media smaller
|
||||
if (reduced) {
|
||||
style.height = width / 2 || '50%'
|
||||
}
|
||||
// if (reduced) {
|
||||
// style.height = width / 2 || '50%'
|
||||
// }
|
||||
|
||||
if (!visible) {
|
||||
style.height = 'auto'
|
||||
@@ -573,7 +574,7 @@ class MediaGallery extends PureComponent {
|
||||
<Button
|
||||
title={intl.formatMessage(messages.toggle_visible)}
|
||||
icon='hidden'
|
||||
backgroundColor='none'
|
||||
backgroundColor='black'
|
||||
className={[_s.px10, _s.bgBlackOpaque_onHover].join(' ')}
|
||||
onClick={this.handleOpen}
|
||||
/>
|
||||
|
||||
@@ -9,9 +9,9 @@ export default
|
||||
class ConfirmationModal extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.node.isRequired,
|
||||
message: PropTypes.node.isRequired,
|
||||
confirm: PropTypes.string.isRequired,
|
||||
title: PropTypes.any.isRequired,
|
||||
message: PropTypes.any.isRequired,
|
||||
confirm: PropTypes.any.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
secondary: PropTypes.string,
|
||||
|
||||
@@ -49,8 +49,9 @@ export const mapDispatchToProps = (dispatch, { onClose }) => ({
|
||||
}
|
||||
},
|
||||
|
||||
handleSelectResult: (resultId) => {
|
||||
|
||||
handleSelectResult: (result) => {
|
||||
dispatch(setSelectedGif(result))
|
||||
onClose()
|
||||
},
|
||||
|
||||
// dispatchSubmit: (e) => {
|
||||
@@ -70,11 +71,11 @@ class GifPickerModal extends PureComponent {
|
||||
handleCloseModal: PropTypes.func.isRequired,
|
||||
handleFetchCategories: PropTypes.func.isRequired,
|
||||
handleOnChange: PropTypes.func.isRequired,
|
||||
handleSelectResult: PropTypes.func.isRequired,
|
||||
categories: PropTypes.array.isRequired,
|
||||
results: PropTypes.array.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.bool,
|
||||
chosenUrl: PropTypes.string,
|
||||
searchText: PropTypes.string,
|
||||
}
|
||||
|
||||
@@ -98,8 +99,8 @@ class GifPickerModal extends PureComponent {
|
||||
this.props.handleOnChange(category)
|
||||
}
|
||||
|
||||
handleSelectGifResult = (resultId) => {
|
||||
console.log("handleSelectGifResult:", resultId)
|
||||
handleSelectGifResult = (resultBlock) => {
|
||||
this.props.handleSelectResult(resultBlock)
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -109,7 +110,7 @@ class GifPickerModal extends PureComponent {
|
||||
results,
|
||||
loading,
|
||||
error,
|
||||
searchText
|
||||
searchText,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
@@ -165,6 +166,7 @@ class GifPickerModal extends PureComponent {
|
||||
}
|
||||
|
||||
class GifResultsCollectionColumn extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
results: PropTypes.array.isRequired,
|
||||
handleSelectGifResult: PropTypes.func.isRequired,
|
||||
@@ -183,8 +185,8 @@ class GifResultsCollectionColumn extends PureComponent {
|
||||
results.map((result, i) => (
|
||||
<button
|
||||
key={`gif-result-item-${i}`}
|
||||
onClick={() => this.onClick(result.id)}
|
||||
className={[_s.default, _s.cursorPointer, _s.px2, _s.py2].join(' ')}
|
||||
onClick={() => this.onClick(result)}
|
||||
className={[_s.default, _s.outlineNone, _s.bgTransparent, _s.cursorPointer, _s.px2, _s.py2].join(' ')}
|
||||
>
|
||||
<Image
|
||||
height={result.media[0].tinygif.dims[1]}
|
||||
@@ -196,9 +198,11 @@ class GifResultsCollectionColumn extends PureComponent {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class GifResultsCollection extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
results: PropTypes.array.isRequired,
|
||||
handleSelectGifResult: PropTypes.func.isRequired,
|
||||
@@ -250,7 +254,7 @@ class GifCategoriesCollection extends PureComponent {
|
||||
<button
|
||||
key={`gif-category-${i}`}
|
||||
onClick={() => this.onClick(category.searchterm)}
|
||||
className={[_s.default, _s.px2, _s.py2, _s.width50PC].join(' ')}
|
||||
className={[_s.default, _s.outlineNone, _s.bgTransparent, _s.px2, _s.py2, _s.width50PC].join(' ')}
|
||||
>
|
||||
<div className={[_s.default, _s.cursorPointer].join(' ')}>
|
||||
<Image
|
||||
@@ -269,4 +273,5 @@ class GifCategoriesCollection extends PureComponent {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -158,19 +158,23 @@ class MediaModal extends ImmutablePureComponent {
|
||||
pagination = media.map((item, i) => {
|
||||
const btnClasses = CX({
|
||||
default: 1,
|
||||
px5: 1,
|
||||
py5: 1,
|
||||
width10PX: 1,
|
||||
height10PX: 1,
|
||||
outlineNone: 1,
|
||||
colorPrimary: 1,
|
||||
circle: 1,
|
||||
cursorPointer: 1,
|
||||
colorPrimary: i === index,
|
||||
lineHeight0825: i === index,
|
||||
bgPrimaryOpaque: i !== index,
|
||||
bgPrimary: i === index,
|
||||
})
|
||||
const activeText = i === index ? '•' : ''
|
||||
|
||||
return (
|
||||
<li className={[_s.default, _s.px5].join(' ')} key={`media-pagination-${i}`}>
|
||||
<button tabIndex='0' className={btnClasses} onClick={this.handleChangeIndex} data-index={i} />
|
||||
<button tabIndex='0' className={btnClasses} onClick={this.handleChangeIndex} data-index={i}>
|
||||
{activeText}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
@@ -234,6 +238,9 @@ class MediaModal extends ImmutablePureComponent {
|
||||
const swipeableViewsStyle = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}
|
||||
|
||||
const navigationClasses = CX({
|
||||
@@ -244,7 +251,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||
return (
|
||||
<div className={[_s.default, _s.width100PC, _s.height100PC, _s.alignItemsCenter, _s.justifyContentCenter].join(' ')}>
|
||||
<div
|
||||
className={[_s.default, _s.top0, _s.right0, _s.bottom0, _s.left0].join(' ')}
|
||||
className={[_s.default, _s.posAbs, _s.top0, _s.right0, _s.bottom0, _s.left0].join(' ')}
|
||||
role='presentation'
|
||||
onClick={onClose}
|
||||
>
|
||||
@@ -252,6 +259,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||
style={swipeableViewsStyle}
|
||||
containerStyle={{
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
onChangeIndex={this.handleSwipe}
|
||||
onSwitching={this.handleSwitching}
|
||||
@@ -286,7 +294,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||
|
||||
</div>
|
||||
|
||||
<ul className={[_s.default, _s.posAbsolute, _s.bottom0, _s.mb15, _s.flexRow, _s.bgBlackOpaque, _s.circle, _s.py10, _s.px15, _s.listStyleNone].join(' ')}>
|
||||
<ul className={[_s.default, _s.posAbs, _s.bottom0, _s.mb15, _s.flexRow, _s.bgBlackOpaque, _s.circle, _s.py10, _s.px15, _s.listStyleNone].join(' ')}>
|
||||
{pagination}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +135,7 @@ class ModalRoot extends PureComponent {
|
||||
}
|
||||
|
||||
renderLoading = () => {
|
||||
return <ModalLoading />
|
||||
return null
|
||||
}
|
||||
|
||||
renderError = () => {
|
||||
|
||||
@@ -45,12 +45,7 @@ class ProUpgradeModal extends ImmutablePureComponent {
|
||||
</div>
|
||||
|
||||
<Button
|
||||
backgroundColor='brand'
|
||||
color='white'
|
||||
icon='pro'
|
||||
href='https://pro.gab.com'
|
||||
className={_s.justifyContentCenter}
|
||||
iconClassName={[_s.mr5, _s.fillWhite].join(' ')}
|
||||
>
|
||||
<Text color='inherit' weight='bold' align='center'>
|
||||
{intl.formatMessage(messages.title)}
|
||||
|
||||
@@ -56,8 +56,8 @@ class ReportModal extends ImmutablePureComponent {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
handleCommentChange = e => {
|
||||
this.props.dispatch(changeReportComment(e.target.value))
|
||||
handleCommentChange = (e) => {
|
||||
this.props.dispatch(changeReportComment(value))
|
||||
}
|
||||
|
||||
handleForwardChange = e => {
|
||||
|
||||
@@ -75,9 +75,11 @@ class Poll extends ImmutablePureComponent {
|
||||
const { selected } = this.state
|
||||
const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100
|
||||
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'))
|
||||
const optionHasNoVotes = option.get('votes_count') === 0
|
||||
const active = !!selected[`${optionIndex}`]
|
||||
const showResults = poll.get('voted') || poll.get('expired')
|
||||
const multiple = poll.get('multiple')
|
||||
const correctedWidthPercent = optionHasNoVotes ? 100 : percent
|
||||
|
||||
let titleEmojified = option.get('title_emojified')
|
||||
if (!titleEmojified) {
|
||||
@@ -92,10 +94,12 @@ class Poll extends ImmutablePureComponent {
|
||||
left0: 1,
|
||||
radiusSmall: 1,
|
||||
height100PC: 1,
|
||||
bgSecondary: !leading,
|
||||
bgSecondary: !leading && !optionHasNoVotes,
|
||||
bgTertiary: !leading && optionHasNoVotes,
|
||||
bgBrandLight: leading,
|
||||
})
|
||||
|
||||
// : todo :
|
||||
const inputClasses = cx('poll__input', {
|
||||
'poll__input--checkbox': multiple,
|
||||
'poll__input--active': active,
|
||||
@@ -107,7 +111,7 @@ class Poll extends ImmutablePureComponent {
|
||||
py10: showResults,
|
||||
mb10: 1,
|
||||
border1PX: !showResults,
|
||||
fillSecondary: !showResults,
|
||||
borderColorSecondary: !showResults,
|
||||
circle: !showResults,
|
||||
cursorPointer: !showResults,
|
||||
bgSubtle_onHover: !showResults,
|
||||
@@ -127,7 +131,7 @@ class Poll extends ImmutablePureComponent {
|
||||
<li className={listItemClasses} key={option.get('title')}>
|
||||
{
|
||||
showResults && (
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(correctedWidthPercent, { stiffness: 180, damping: 24 }) }}>
|
||||
{({ width }) =>
|
||||
<span className={chartClasses} style={{ width: `${width}%` }} />
|
||||
}
|
||||
@@ -139,7 +143,7 @@ class Poll extends ImmutablePureComponent {
|
||||
<Text
|
||||
size='medium'
|
||||
color='primary'
|
||||
weight={leading ? 'bold' : 'normal'}
|
||||
weight={(leading && showResults) ? 'bold' : 'normal'}
|
||||
className={[_s.displayFlex, _s.flexRow, _s.width100PC, _s.alignItemsCenter].join(' ')}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import DatePicker from 'react-datepicker'
|
||||
import { FormattedMessage } from 'react-intl'
|
||||
import moment from 'moment-mini'
|
||||
import { changeScheduledAt } from '../../actions/compose'
|
||||
import { openModal } from '../../actions/modal'
|
||||
import { closePopover } from '../../actions/popover'
|
||||
import { me } from '../../initial_state'
|
||||
import {
|
||||
MODAL_PRO_UPGRADE,
|
||||
} from '../../constants'
|
||||
import { isMobile } from '../../utils/is_mobile'
|
||||
import PopoverLayout from './popover_layout'
|
||||
import Button from '../button'
|
||||
import Text from '../text'
|
||||
|
||||
import '!style-loader!css-loader!react-datepicker/dist/react-datepicker.css'
|
||||
|
||||
@@ -11,9 +20,18 @@ const mapStateToProps = (state) => ({
|
||||
isPro: state.getIn(['accounts', me, 'is_pro']),
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = (dispatch, { isPro }) => ({
|
||||
setScheduledAt (date) {
|
||||
if (!isPro) {
|
||||
dispatch(closePopover())
|
||||
return dispatch(openModal(MODAL_PRO_UPGRADE))
|
||||
}
|
||||
|
||||
dispatch(changeScheduledAt(date))
|
||||
|
||||
if (!date) {
|
||||
dispatch(closePopover())
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -31,42 +49,70 @@ class DatePickerPopover extends PureComponent {
|
||||
handleSetDate = (date) => {
|
||||
this.props.setScheduledAt(date)
|
||||
}
|
||||
|
||||
handleRemoveDate = () => {
|
||||
this.props.setScheduledAt(null)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { date, isPro, position } = this.props
|
||||
const { date, isPro } = this.props
|
||||
|
||||
const open = !!date
|
||||
const datePickerDisabled = !isPro
|
||||
const withPortal = isMobile(window.innerWidth)
|
||||
|
||||
return (
|
||||
<PopoverLayout width={331}>
|
||||
<DatePicker
|
||||
inline
|
||||
target={this}
|
||||
className='schedule-post-dropdown__datepicker'
|
||||
minDate={new Date()}
|
||||
selected={date}
|
||||
onChange={date => this.handleSetDate(date)}
|
||||
timeFormat='p'
|
||||
timeIntervals={15}
|
||||
timeCaption='Time'
|
||||
dateFormat='MMM d, yyyy h:mm aa'
|
||||
disabled={datePickerDisabled}
|
||||
showTimeSelect
|
||||
withPortal={withPortal}
|
||||
popperModifiers={{
|
||||
offset: {
|
||||
enabled: true,
|
||||
offset: '0px, 5px'
|
||||
},
|
||||
preventOverflow: {
|
||||
enabled: true,
|
||||
escapeWithReference: false,
|
||||
boundariesElement: 'viewport'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className={[_s.default].join(' ')}>
|
||||
<DatePicker
|
||||
inline
|
||||
target={this}
|
||||
className='schedule-post-dropdown__datepicker'
|
||||
minDate={new Date()}
|
||||
selected={date}
|
||||
onChange={date => this.handleSetDate(date)}
|
||||
timeFormat='p'
|
||||
timeIntervals={15}
|
||||
timeCaption='Time'
|
||||
dateFormat='MMM d, yyyy h:mm aa'
|
||||
disabled={datePickerDisabled}
|
||||
showTimeSelect
|
||||
withPortal={withPortal}
|
||||
popperModifiers={{
|
||||
offset: {
|
||||
enabled: true,
|
||||
offset: '0px, 5px'
|
||||
},
|
||||
preventOverflow: {
|
||||
enabled: true,
|
||||
escapeWithReference: false,
|
||||
boundariesElement: 'viewport'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
date &&
|
||||
<div className={[_s.default, _s.alignItemsCenter, _s.flexRow, _s.px10, _s.py10, _s.borderTop1PX, _s.borderColorSecondary].join(' ')}>
|
||||
<Text size='extraSmall' color='secondary'>
|
||||
<FormattedMessage id='scheduled_for_datetime' defaultMessage='Scheduled for {datetime}' values={{
|
||||
datetime: moment.utc(date).format('lll'),
|
||||
}}/>
|
||||
</Text>
|
||||
<div className={_s.mlAuto}>
|
||||
<Button
|
||||
isNarrow
|
||||
radiusSmall
|
||||
color='primary'
|
||||
backgroundColor='tertiary'
|
||||
onClick={this.handleRemoveDate}
|
||||
>
|
||||
<Text color='inherit' size='small'>
|
||||
<FormattedMessage id='remove' defaultMessage='Remove' />
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</PopoverLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import detectPassiveEvents from 'detect-passive-events'
|
||||
import { changeSetting } from '../../actions/settings'
|
||||
import { useEmoji } from '../../actions/emojis'
|
||||
import { closePopover } from '../../actions/popover'
|
||||
import { insertEmojiCompose } from '../../actions/compose'
|
||||
import { EmojiPicker as EmojiPickerAsync } from '../../features/ui/util/async_components'
|
||||
import { buildCustomEmojis } from '../emoji/emoji'
|
||||
import PopoverLayout from './popover_layout'
|
||||
@@ -209,21 +210,19 @@ const mapStateToProps = (state) => ({
|
||||
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onClosePopover() {
|
||||
dispatch(closePopover())
|
||||
},
|
||||
|
||||
onSkinTone: skinTone => {
|
||||
onSkinTone: (skinTone) => {
|
||||
dispatch(changeSetting(['skinTone'], skinTone))
|
||||
},
|
||||
|
||||
onPickEmoji: emoji => {
|
||||
onPickEmoji: (emoji) => {
|
||||
dispatch(useEmoji(emoji))
|
||||
|
||||
if (onPickEmoji) {
|
||||
onPickEmoji(emoji)
|
||||
}
|
||||
console.log("emoji:", emoji)
|
||||
dispatch(insertEmojiCompose(0, emoji, false))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -274,7 +273,7 @@ class EmojiPickerPopover extends ImmutablePureComponent {
|
||||
} = this.props
|
||||
|
||||
const { loading } = this.state
|
||||
|
||||
|
||||
return (
|
||||
<PopoverLayout width={340}>
|
||||
<EmojiPickerMenu
|
||||
|
||||
@@ -57,12 +57,6 @@ class PopoverRoot extends PureComponent {
|
||||
props: PropTypes.object,
|
||||
}
|
||||
|
||||
getSnapshotBeforeUpdate() {
|
||||
return {
|
||||
visible: !!this.props.type
|
||||
}
|
||||
}
|
||||
|
||||
renderEmpty = () => {
|
||||
return <div />
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ export default class ScrollableList extends PureComponent {
|
||||
}
|
||||
|
||||
handleLoadMore = (e) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
this.props.onLoadMore();
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,10 @@ export default
|
||||
@injectIntl
|
||||
class Sidebar extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
account: ImmutablePropTypes.map,
|
||||
@@ -94,6 +98,18 @@ class Sidebar extends ImmutablePureComponent {
|
||||
})
|
||||
}
|
||||
|
||||
historyBack = () => {
|
||||
if (window.history && window.history.length === 1) {
|
||||
this.context.router.history.push('/home')
|
||||
} else {
|
||||
this.context.router.history.goBack()
|
||||
}
|
||||
}
|
||||
|
||||
handleBackClick = () => {
|
||||
this.historyBack()
|
||||
}
|
||||
|
||||
setMoreButtonRef = n => {
|
||||
this.moreBtnRef = n
|
||||
}
|
||||
@@ -213,7 +229,17 @@ class Sidebar extends ImmutablePureComponent {
|
||||
<div className={_s.default}>
|
||||
{
|
||||
!!title &&
|
||||
<div className={[_s.default, _s.px5, _s.py10].join(' ')}>
|
||||
<div className={[_s.default, _s.flexRow, _s.px5, _s.py10].join(' ')}>
|
||||
<Button
|
||||
noClasses
|
||||
color='primary'
|
||||
backgroundColor='none'
|
||||
className={[_s.alignItemsCenter, _s.bgTransparent, _s.mr5, _s.cursorPointer, _s.outlineNone, _s.default, _s.justifyContentCenter].join(' ')}
|
||||
icon='back'
|
||||
iconSize='20px'
|
||||
iconClassName={[_s.mr5, _s.fillPrimary].join(' ')}
|
||||
onClick={this.handleBackClick}
|
||||
/>
|
||||
<Heading size='h1'>
|
||||
{title}
|
||||
</Heading>
|
||||
|
||||
@@ -10,6 +10,7 @@ import ComposeFormContainer from '../features/compose/containers/compose_form_co
|
||||
import StatusContent from './status_content'
|
||||
import StatusPrepend from './status_prepend'
|
||||
import StatusActionBar from './status_action_bar'
|
||||
import StatusMedia from './status_media'
|
||||
import Poll from './poll'
|
||||
import StatusHeader from './status_header'
|
||||
import CommentList from './comment_list'
|
||||
@@ -87,6 +88,7 @@ class Status extends ImmutablePureComponent {
|
||||
cacheMediaWidth: PropTypes.func,
|
||||
cachedMediaWidth: PropTypes.number,
|
||||
contextType: PropTypes.string,
|
||||
commentsLimited: PropTypes.bool,
|
||||
}
|
||||
|
||||
// Avoid checking props that are functions (and whose equality will always
|
||||
@@ -124,7 +126,7 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (!nextProps.isHidden && nextProps.isIntersecting && !prevState.loadedComments) {
|
||||
if (!nextProps.isHidden && (nextProps.isIntersecting || !nextProps.commentsLimited) && !prevState.loadedComments) {
|
||||
return {
|
||||
loadedComments: true
|
||||
}
|
||||
@@ -132,6 +134,7 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
|
||||
return {
|
||||
loadedComments: false,
|
||||
showMedia: defaultMediaVisibility(nextProps.status),
|
||||
statusId: nextProps.status.get('id'),
|
||||
}
|
||||
@@ -142,6 +145,7 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
// Compensate height changes
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
// timeline lazy loading comments
|
||||
if (!prevState.loadedComments && this.state.loadedComments && this.props.status) {
|
||||
const commentCount = this.props.status.get('replies_count')
|
||||
if (commentCount > 0) {
|
||||
@@ -163,7 +167,7 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleMoveUp = id => {
|
||||
handleMoveUp = (id) => {
|
||||
const { status, ancestorsIds, descendantsIds } = this.props
|
||||
|
||||
if (id === status.get('id')) {
|
||||
@@ -347,7 +351,7 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
if (!status) return null
|
||||
|
||||
let media, reblogContent, rebloggedByText = null
|
||||
let reblogContent, rebloggedByText = null
|
||||
|
||||
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
||||
rebloggedByText = intl.formatMessage(
|
||||
@@ -386,62 +390,6 @@ class Status extends ImmutablePureComponent {
|
||||
return null
|
||||
}
|
||||
|
||||
if (status.get('poll')) {
|
||||
media = <Poll pollId={status.get('poll')} />
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const video = status.getIn(['media_attachments', 0])
|
||||
|
||||
media = (
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingMedia}>
|
||||
{Component => (
|
||||
<Component
|
||||
inline
|
||||
preview={video.get('preview_url')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
aspectRatio={video.getIn(['meta', 'small', 'aspect'])}
|
||||
width={this.props.cachedMediaWidth}
|
||||
height={110}
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
)
|
||||
} else {
|
||||
media = (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMedia}>
|
||||
{Component => (
|
||||
<Component
|
||||
reduced={isChild}
|
||||
media={status.get('media_attachments')}
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenMedia={this.props.onOpenMedia}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
)
|
||||
}
|
||||
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
|
||||
media = (
|
||||
<StatusCard
|
||||
onOpenMedia={this.props.onOpenMedia}
|
||||
card={status.get('card')}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const containerClasses = cx({
|
||||
default: 1,
|
||||
pb15: isFeatured,
|
||||
@@ -476,7 +424,7 @@ class Status extends ImmutablePureComponent {
|
||||
data-featured={isFeatured ? 'true' : null}
|
||||
aria-label={textForScreenReader(intl, status, rebloggedByText)}
|
||||
ref={this.handleRef}
|
||||
// onClick={this.handleClick}
|
||||
onClick={isChild ? this.handleClick : undefined}
|
||||
>
|
||||
<div className={innerContainerClasses}>
|
||||
|
||||
@@ -497,7 +445,17 @@ class Status extends ImmutablePureComponent {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{media}
|
||||
<StatusMedia
|
||||
isChild={isChild}
|
||||
status={status}
|
||||
onOpenMedia={this.props.onOpenMedia}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
width={this.props.cachedMediaWidth}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
/>
|
||||
|
||||
{
|
||||
!!status.get('quote') && !isChild &&
|
||||
|
||||
@@ -42,7 +42,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
updateOnProps = ['status']
|
||||
|
||||
handleReplyClick = () => {
|
||||
this.props.onReply(this.props.status)
|
||||
this.props.onReply(this.props.status, null, true)
|
||||
}
|
||||
|
||||
handleFavoriteClick = () => {
|
||||
|
||||
@@ -10,8 +10,8 @@ import Text from './text'
|
||||
const MAX_HEIGHT = 200
|
||||
|
||||
const messages = defineMessages({
|
||||
showMore: { id: 'status.show_more', defaultMessage: 'Show more' },
|
||||
showLess: { id: 'status.show_less', defaultMessage: 'Show less' },
|
||||
show: { id: 'status.show_more', defaultMessage: 'Show' },
|
||||
hide: { id: 'status.show_less', defaultMessage: 'Hide' },
|
||||
readMore: { id: 'status.read_more', defaultMessage: 'Read more' },
|
||||
})
|
||||
|
||||
@@ -220,7 +220,7 @@ class StatusContent extends ImmutablePureComponent {
|
||||
)
|
||||
}
|
||||
|
||||
const toggleText = intl.formatMessage(hidden ? messages.showMore : messages.showLess)
|
||||
const toggleText = intl.formatMessage(hidden ? messages.show : messages.hide)
|
||||
|
||||
const spoilerContainerClasses = cx({
|
||||
default: 1,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Fragment } from 'react'
|
||||
import { injectIntl, defineMessages } from 'react-intl'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
@@ -14,6 +15,14 @@ import Icon from './icon'
|
||||
import Button from './button'
|
||||
import Avatar from './avatar'
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for anyone on or off Gab' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
|
||||
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for your followers only' },
|
||||
})
|
||||
|
||||
const cx = classNames.bind(_s)
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
@@ -33,10 +42,12 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
})
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
@connect(null, mapDispatchToProps)
|
||||
class StatusHeader extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
status: ImmutablePropTypes.map,
|
||||
onOpenStatusRevisionsPopover: PropTypes.func.isRequired,
|
||||
onOpenStatusOptionsPopover: PropTypes.func.isRequired,
|
||||
@@ -56,7 +67,11 @@ class StatusHeader extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, reduced } = this.props
|
||||
const {
|
||||
intl,
|
||||
reduced,
|
||||
status,
|
||||
} = this.props
|
||||
|
||||
const statusUrl = `/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`
|
||||
|
||||
@@ -68,7 +83,22 @@ class StatusHeader extends ImmutablePureComponent {
|
||||
})
|
||||
|
||||
const avatarSize = reduced ? 20 : 46
|
||||
const visibilityIcon = 'globe'
|
||||
|
||||
const visibility = status.get('visibility')
|
||||
|
||||
let visibilityIcon
|
||||
let visibilityText
|
||||
|
||||
if (visibility === 'private') {
|
||||
visibilityIcon = 'lock-filled'
|
||||
visibilityText = intl.formatMessage(messages.private_long)
|
||||
} else if (visibility === 'unlisted') {
|
||||
visibilityIcon = 'unlock-filled'
|
||||
visibilityText = `${intl.formatMessage(messages.unlisted_short)} - ${intl.formatMessage(messages.unlisted_long)}`
|
||||
} else {
|
||||
visibilityIcon = 'globe'
|
||||
visibilityText = `${intl.formatMessage(messages.public_short)} - ${intl.formatMessage(messages.public_long)}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
@@ -126,8 +156,10 @@ class StatusHeader extends ImmutablePureComponent {
|
||||
</Button>
|
||||
|
||||
<DotTextSeperator />
|
||||
|
||||
<Icon id={visibilityIcon} size='12px' className={[_s.default, _s.displayInline, _s.ml5, _s.fillSecondary].join(' ')} />
|
||||
|
||||
<span title={visibilityText} className={[_s.default, _s.displayInline, _s.ml5].join(' ')}>
|
||||
<Icon id={visibilityIcon} size='12px' className={[_s.default, _s.fillSecondary].join(' ')} />
|
||||
</span>
|
||||
|
||||
{
|
||||
!!status.get('group') &&
|
||||
|
||||
120
app/javascript/gabsocial/components/status_media.js
Normal file
120
app/javascript/gabsocial/components/status_media.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import { injectIntl } from 'react-intl'
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import StatusCard from './status_card'
|
||||
import { MediaGallery, Video } from '../features/ui/util/async_components'
|
||||
import Poll from './poll'
|
||||
|
||||
// We use the component (and not the container) since we do not want
|
||||
// to use the progress bar to show download progress
|
||||
import Bundle from '../features/ui/util/bundle'
|
||||
|
||||
export default class StatusMedia extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
isChild: PropTypes.bool,
|
||||
isComment: PropTypes.bool,
|
||||
onOpenMedia: PropTypes.func,
|
||||
onOpenVideo: PropTypes.func,
|
||||
width: PropTypes.number,
|
||||
onToggleVisibility: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
defaultWidth: PropTypes.number,
|
||||
cacheWidth: PropTypes.number,
|
||||
}
|
||||
|
||||
// Avoid checking props that are functions (and whose equality will always
|
||||
// evaluate to false. See react-immutable-pure-component for usage.
|
||||
updateOnProps = [
|
||||
'status',
|
||||
'isChild',
|
||||
'isComment',
|
||||
'cacheWidth',
|
||||
'defaultWidth',
|
||||
'visible',
|
||||
'width',
|
||||
]
|
||||
|
||||
renderLoadingMedia() {
|
||||
return <div className={_s.backgroundColorPanel} style={{ height: '110px' }} />
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
status,
|
||||
isChild,
|
||||
isComment,
|
||||
onOpenMedia,
|
||||
onOpenVideo,
|
||||
width,
|
||||
onToggleVisibility,
|
||||
visible,
|
||||
defaultWidth,
|
||||
cacheWidth,
|
||||
} = this.props
|
||||
|
||||
if (!status) return null
|
||||
|
||||
let media = null
|
||||
|
||||
if (status.get('poll')) {
|
||||
media = <Poll pollId={status.get('poll')} />
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const video = status.getIn(['media_attachments', 0])
|
||||
|
||||
media = (
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingMedia}>
|
||||
{Component => (
|
||||
<Component
|
||||
inline
|
||||
preview={video.get('preview_url')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
aspectRatio={video.getIn(['meta', 'small', 'aspect'])}
|
||||
sensitive={status.get('sensitive')}
|
||||
height={110}
|
||||
width={width}
|
||||
onOpenVideo={onOpenVideo}
|
||||
cacheWidth={cacheWidth}
|
||||
visible={visible}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
)
|
||||
} else {
|
||||
media = (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMedia}>
|
||||
{Component => (
|
||||
<Component
|
||||
reduced={isChild}
|
||||
media={status.get('media_attachments')}
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenMedia={onOpenMedia}
|
||||
cacheWidth={cacheWidth}
|
||||
defaultWidth={defaultWidth}
|
||||
visible={visible}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
)
|
||||
}
|
||||
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
|
||||
media = (
|
||||
<StatusCard
|
||||
card={status.get('card')}
|
||||
onOpenMedia={onOpenMedia}
|
||||
cacheWidth={cacheWidth}
|
||||
defaultWidth={defaultWidth}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return media
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,30 +1,18 @@
|
||||
const MIN_SCALE = 1;
|
||||
const MAX_SCALE = 4;
|
||||
const MIN_SCALE = 1
|
||||
const MAX_SCALE = 4
|
||||
|
||||
const getMidpoint = (p1, p2) => ({
|
||||
x: (p1.clientX + p2.clientX) / 2,
|
||||
y: (p1.clientY + p2.clientY) / 2,
|
||||
});
|
||||
})
|
||||
|
||||
const getDistance = (p1, p2) =>
|
||||
Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2));
|
||||
const getDistance = (p1, p2) => {
|
||||
return Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2))
|
||||
}
|
||||
|
||||
const clamp = (min, max, value) => Math.min(max, Math.max(min, value));
|
||||
|
||||
// .zoomable-image {
|
||||
// position: relative;
|
||||
|
||||
// @include flex(center, center);
|
||||
// @include size(100%);
|
||||
|
||||
// img {
|
||||
// object-fit: contain;
|
||||
|
||||
// @include size(auto);
|
||||
// @include max-size($media-modal-media-max-width, $media-modal-media-max-height);
|
||||
// }
|
||||
// }
|
||||
// : todo :
|
||||
const clamp = (min, max, value) => {
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
|
||||
export default class ZoomableImage extends PureComponent {
|
||||
|
||||
@@ -40,69 +28,69 @@ export default class ZoomableImage extends PureComponent {
|
||||
alt: '',
|
||||
width: null,
|
||||
height: null,
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
scale: MIN_SCALE,
|
||||
}
|
||||
|
||||
removers = [];
|
||||
container = null;
|
||||
image = null;
|
||||
lastTouchEndTime = 0;
|
||||
lastDistance = 0;
|
||||
removers = []
|
||||
container = null
|
||||
image = null
|
||||
lastTouchEndTime = 0
|
||||
lastDistance = 0
|
||||
|
||||
componentDidMount () {
|
||||
let handler = this.handleTouchStart;
|
||||
this.container.addEventListener('touchstart', handler);
|
||||
this.removers.push(() => this.container.removeEventListener('touchstart', handler));
|
||||
handler = this.handleTouchMove;
|
||||
let handler = this.handleTouchStart
|
||||
this.container.addEventListener('touchstart', handler)
|
||||
this.removers.push(() => this.container.removeEventListener('touchstart', handler))
|
||||
handler = this.handleTouchMove
|
||||
// on Chrome 56+, touch event listeners will default to passive
|
||||
// https://www.chromestatus.com/features/5093566007214080
|
||||
this.container.addEventListener('touchmove', handler, { passive: false });
|
||||
this.removers.push(() => this.container.removeEventListener('touchend', handler));
|
||||
this.container.addEventListener('touchmove', handler, { passive: false })
|
||||
this.removers.push(() => this.container.removeEventListener('touchend', handler))
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.removeEventListeners();
|
||||
this.removeEventListeners()
|
||||
}
|
||||
|
||||
removeEventListeners () {
|
||||
this.removers.forEach(listeners => listeners());
|
||||
this.removers = [];
|
||||
this.removers.forEach(listeners => listeners())
|
||||
this.removers = []
|
||||
}
|
||||
|
||||
handleTouchStart = e => {
|
||||
if (e.touches.length !== 2) return;
|
||||
if (e.touches.length !== 2) return
|
||||
|
||||
this.lastDistance = getDistance(...e.touches);
|
||||
this.lastDistance = getDistance(...e.touches)
|
||||
}
|
||||
|
||||
handleTouchMove = e => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = this.container;
|
||||
const { scrollTop, scrollHeight, clientHeight } = this.container
|
||||
if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) {
|
||||
// prevent propagating event to MediaModal
|
||||
e.stopPropagation();
|
||||
return;
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (e.touches.length !== 2) return;
|
||||
if (e.touches.length !== 2) return
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const distance = getDistance(...e.touches);
|
||||
const midpoint = getMidpoint(...e.touches);
|
||||
const scale = clamp(MIN_SCALE, MAX_SCALE, this.state.scale * distance / this.lastDistance);
|
||||
const distance = getDistance(...e.touches)
|
||||
const midpoint = getMidpoint(...e.touches)
|
||||
const scale = clamp(MIN_SCALE, MAX_SCALE, this.state.scale * distance / this.lastDistance)
|
||||
|
||||
this.zoom(scale, midpoint);
|
||||
this.zoom(scale, midpoint)
|
||||
|
||||
this.lastMidpoint = midpoint;
|
||||
this.lastDistance = distance;
|
||||
this.lastMidpoint = midpoint
|
||||
this.lastDistance = distance
|
||||
}
|
||||
|
||||
zoom(nextScale, midpoint) {
|
||||
const { scale } = this.state;
|
||||
const { scrollLeft, scrollTop } = this.container;
|
||||
const { scale } = this.state
|
||||
const { scrollLeft, scrollTop } = this.container
|
||||
|
||||
// math memo:
|
||||
// x = (scrollLeft + midpoint.x) / scrollWidth
|
||||
@@ -110,38 +98,44 @@ export default class ZoomableImage extends PureComponent {
|
||||
// scrollWidth = clientWidth * scale
|
||||
// scrollWidth' = clientWidth * nextScale
|
||||
// Solve x = x' for nextScrollLeft
|
||||
const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x;
|
||||
const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y;
|
||||
const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x
|
||||
const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y
|
||||
|
||||
this.setState({ scale: nextScale }, () => {
|
||||
this.container.scrollLeft = nextScrollLeft;
|
||||
this.container.scrollTop = nextScrollTop;
|
||||
});
|
||||
this.container.scrollLeft = nextScrollLeft
|
||||
this.container.scrollTop = nextScrollTop
|
||||
})
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
// don't propagate event to MediaModal
|
||||
e.stopPropagation();
|
||||
const handler = this.props.onClick;
|
||||
if (handler) handler();
|
||||
e.stopPropagation()
|
||||
const handler = this.props.onClick
|
||||
if (handler) handler()
|
||||
}
|
||||
|
||||
setContainerRef = c => {
|
||||
this.container = c;
|
||||
this.container = c
|
||||
}
|
||||
|
||||
setImageRef = c => {
|
||||
this.image = c;
|
||||
this.image = c
|
||||
}
|
||||
|
||||
render () {
|
||||
const { alt, src } = this.props;
|
||||
const { scale } = this.state;
|
||||
const overflow = scale === 1 ? 'hidden' : 'scroll';
|
||||
const { alt, src } = this.props
|
||||
const { scale } = this.state
|
||||
|
||||
const overflow = scale === 1 ? 'hidden' : 'scroll'
|
||||
|
||||
return (
|
||||
<div className='zoomable-image' ref={this.setContainerRef} style={{ overflow }}>
|
||||
<div
|
||||
className={[_s.default, _s.width100PC, _s.height100PC, _s.alignItemsCenter, _s.justifyContentCenter].join(' ')}
|
||||
ref={this.setContainerRef}
|
||||
style={{ overflow }}
|
||||
>
|
||||
<img
|
||||
className={[_s.default, _s.objectFitContain, _s.heightAuto, _s.widthAuto, _s.maxWidth100PC, _s.heightMax100PC].join(' ')}
|
||||
role='presentation'
|
||||
ref={this.setImageRef}
|
||||
alt={alt}
|
||||
@@ -154,7 +148,7 @@ export default class ZoomableImage extends PureComponent {
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user