Progress
This commit is contained in:
132
app/javascript/gabsocial/components/media_item.js
Normal file
132
app/javascript/gabsocial/components/media_item.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { decode } from 'blurhash'
|
||||
import { autoPlayGif, displayMedia } from '../initial_state'
|
||||
import Icon from './icon'
|
||||
import Image from './image'
|
||||
import Text from './text'
|
||||
|
||||
export default class MediaItem extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
attachment: ImmutablePropTypes.map.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
|
||||
loaded: false,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.attachment.get('blurhash')) {
|
||||
this._decode()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
|
||||
this._decode()
|
||||
}
|
||||
}
|
||||
|
||||
_decode() {
|
||||
const hash = this.props.attachment.get('blurhash')
|
||||
const pixels = decode(hash, 160, 160)
|
||||
|
||||
if (pixels && this.canvas) {
|
||||
const ctx = this.canvas.getContext('2d')
|
||||
const imageData = new ImageData(pixels, 160, 160)
|
||||
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
setCanvasRef = c => {
|
||||
this.canvas = c
|
||||
}
|
||||
|
||||
handleImageLoad = () => {
|
||||
this.setState({ loaded: true })
|
||||
}
|
||||
|
||||
hoverToPlay() {
|
||||
return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1
|
||||
}
|
||||
|
||||
render() {
|
||||
const { attachment } = this.props
|
||||
const { visible, loaded } = this.state
|
||||
|
||||
const status = attachment.get('status')
|
||||
const title = status.get('spoiler_text') || attachment.get('description')
|
||||
|
||||
const attachmentType = attachment.get('type')
|
||||
let badge = null
|
||||
|
||||
if (attachmentType === 'video') {
|
||||
const duration = attachment.getIn(['meta', 'duration'])
|
||||
badge = (duration / 60).toFixed(2)
|
||||
} else if (attachmentType === 'gifv') {
|
||||
badge = 'GIF'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={[_s.default, _s.width25PC, _s.paddingTop25PC].join(' ')}>
|
||||
<div className={[_s.default, _s.positionAbsolute, _s.top0, _s.height100PC, _s.width100PC, _s.paddingVertical5PX, _s.paddingHorizontal5PX].join(' ')}>
|
||||
<NavLink
|
||||
to={status.get('url')} /* : todo : */
|
||||
title={title}
|
||||
className={[_s.default, _s.width100PC, _s.height100PC, _s.border1PX, _s.borderColorSecondary, _s.overflowHidden].join(' ')}
|
||||
>
|
||||
{
|
||||
(!loaded || !visible) &&
|
||||
<canvas
|
||||
height='100%'
|
||||
width='100%'
|
||||
ref={this.setCanvasRef}
|
||||
className={[_s.default, _s.width100PC, _s.height100PC, _s.z2].join(' ')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
visible &&
|
||||
<Image
|
||||
height='100%'
|
||||
src={attachment.get('preview_url')}
|
||||
alt={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
onLoad={this.handleImageLoad}
|
||||
className={_s.z1}
|
||||
/>
|
||||
}
|
||||
|
||||
<div className={[_s.default, _s.alignItemsCenter, _s.justifyContentCenter, _s.height100PC, _s.width100PC, _s.z3, _s.positionAbsolute].join(' ')}>
|
||||
{
|
||||
!visible &&
|
||||
<Icon
|
||||
id='hidden'
|
||||
width='22px'
|
||||
height='22px'
|
||||
className={[_s.fillColorWhite].join('')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!badge &&
|
||||
<div className={[_s.default, _s.positionAbsolute, _s.radiusSmall, _s.backgroundColorOpaque, _s.paddingHorizontal5PX, _s.paddingVertical5PX, _s.marginRight5PX, _s.marginVertical5PX, _s.bottom0, _s.right0].join(' ')}>
|
||||
<Text size='extraSmall' color='white'>
|
||||
{badge}
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class BoostModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
onReblog: PropTypes.func.isRequired,
|
||||
onRepost: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -33,8 +33,8 @@ class BoostModal extends ImmutablePureComponent {
|
||||
this.button.focus();
|
||||
}
|
||||
|
||||
handleReblog = () => {
|
||||
this.props.onReblog(this.props.status);
|
||||
handleRepost = () => {
|
||||
this.props.onRepost(this.props.status);
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ class BoostModal extends ImmutablePureComponent {
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
|
||||
<Button text={intl.formatMessage(buttonText)} onClick={this.handleRepost} ref={this.setRef} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ const messages = defineMessages({
|
||||
reply: { id: 'keyboard_shortcuts.reply', defaultMessage: 'reply' },
|
||||
mention: { id: 'keyboard_shortcuts.mention', defaultMessage: 'mention author' },
|
||||
profile: { id: 'keyboard_shortcuts.profile', defaultMessage: 'open author\'s profile' },
|
||||
favourite: { id: 'keyboard_shortcuts.favourite', defaultMessage: 'favorite' },
|
||||
favorite: { id: 'keyboard_shortcuts.favorite', defaultMessage: 'favorite' },
|
||||
boost: { id: 'keyboard_shortcuts.boost', defaultMessage: 'repost' },
|
||||
enter: { id: 'keyboard_shortcuts.enter', defaultMessage: 'open status' },
|
||||
toggle_hidden: { id: 'keyboard_shortcuts.toggle_hidden', defaultMessage: 'show/hide text behind CW' },
|
||||
@@ -28,7 +28,7 @@ const messages = defineMessages({
|
||||
notifications: { id: 'keyboard_shortcuts.notifications', defaultMessage: 'open notifications column' },
|
||||
direct: { id: 'keyboard_shortcuts.direct', defaultMessage: 'open direct messages column' },
|
||||
start: { id: 'keyboard_shortcuts.start', defaultMessage: 'open "get started" column' },
|
||||
favourites: { id: 'keyboard_shortcuts.favourites', defaultMessage: 'open favorites list' },
|
||||
favorites: { id: 'keyboard_shortcuts.favorites', defaultMessage: 'open favorites list' },
|
||||
pinned: { id: 'keyboard_shortcuts.pinned', defaultMessage: 'open pinned gabs list' },
|
||||
my_profile: { id: 'keyboard_shortcuts.my_profile', defaultMessage: 'open your profile' },
|
||||
blocked: { id: 'keyboard_shortcuts.blocked', defaultMessage: 'open blocked users list' },
|
||||
@@ -65,7 +65,7 @@ class HotkeysModal extends ImmutablePureComponent {
|
||||
<HotKeysModalRow hotkey='r' action={intl.formatMessage(messages.reply)} />
|
||||
<HotKeysModalRow hotkey='m' action={intl.formatMessage(messages.mention)} />
|
||||
<HotKeysModalRow hotkey='p' action={intl.formatMessage(messages.profile)} />
|
||||
<HotKeysModalRow hotkey='f' action={intl.formatMessage(messages.favourite)} />
|
||||
<HotKeysModalRow hotkey='f' action={intl.formatMessage(messages.favorite)} />
|
||||
<HotKeysModalRow hotkey='b' action={intl.formatMessage(messages.boost)} />
|
||||
<HotKeysModalRow hotkey='enter, o' action={intl.formatMessage(messages.enter)} />
|
||||
<HotKeysModalRow hotkey='x' action={intl.formatMessage(messages.toggle_hidden)} />
|
||||
@@ -108,7 +108,7 @@ class HotkeysModal extends ImmutablePureComponent {
|
||||
</thead>
|
||||
<tbody>
|
||||
<HotKeysModalRow hotkey='g + s' action={intl.formatMessage(messages.start)} />
|
||||
<HotKeysModalRow hotkey='g + f' action={intl.formatMessage(messages.favourites)} />
|
||||
<HotKeysModalRow hotkey='g + f' action={intl.formatMessage(messages.favorites)} />
|
||||
<HotKeysModalRow hotkey='g + p' action={intl.formatMessage(messages.pinned)} />
|
||||
<HotKeysModalRow hotkey='g + u' action={intl.formatMessage(messages.my_profile)} />
|
||||
<HotKeysModalRow hotkey='g + b' action={intl.formatMessage(messages.blocked)} />
|
||||
|
||||
@@ -51,7 +51,7 @@ class ProfileStatsPanel extends ImmutablePureComponent {
|
||||
account.get('id') === me &&
|
||||
<UserStat
|
||||
title={intl.formatMessage(messages.favorites)}
|
||||
value={shortNumberFormat(account.get('favourite_count'))}
|
||||
value={shortNumberFormat(account.get('favourite_count'))} /* : todo : */
|
||||
to={`/${account.get('acct')}/favorites`}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default class StatusOptionsPopover extends PureComponent {
|
||||
// menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||
// } else {
|
||||
// if (status.get('visibility') === 'private') {
|
||||
// menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
|
||||
// menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleRepostClick });
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ export default class ScrollableList extends PureComponent {
|
||||
this.scrollToTopOnMouseIdle = false;
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.window = window;
|
||||
this.documentElement = document.scrollingElement || document.documentElement;
|
||||
|
||||
@@ -97,7 +97,7 @@ export default class ScrollableList extends PureComponent {
|
||||
this.setScrollTop(newScrollTop);
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState, snapshot) {
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
// Reset the scroll position when a new child comes in in order not to
|
||||
// jerk the scrollbar around if you're already scrolled down the page.
|
||||
if (snapshot !== null) {
|
||||
@@ -105,12 +105,12 @@ export default class ScrollableList extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
attachScrollListener () {
|
||||
attachScrollListener() {
|
||||
this.window.addEventListener('scroll', this.handleScroll);
|
||||
this.window.addEventListener('wheel', this.handleWheel);
|
||||
}
|
||||
|
||||
detachScrollListener () {
|
||||
detachScrollListener() {
|
||||
this.window.removeEventListener('scroll', this.handleScroll);
|
||||
this.window.removeEventListener('wheel', this.handleWheel);
|
||||
}
|
||||
@@ -139,16 +139,16 @@ export default class ScrollableList extends PureComponent {
|
||||
this.lastScrollWasSynthetic = false;
|
||||
}
|
||||
}, 150, {
|
||||
trailing: true,
|
||||
});
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
handleWheel = throttle(() => {
|
||||
this.scrollToTopOnMouseIdle = false;
|
||||
}, 150, {
|
||||
trailing: true,
|
||||
});
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
getSnapshotBeforeUpdate (prevProps) {
|
||||
getSnapshotBeforeUpdate(prevProps) {
|
||||
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
|
||||
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
|
||||
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
|
||||
@@ -166,21 +166,21 @@ export default class ScrollableList extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
componentWillUnmount() {
|
||||
this.clearMouseIdleTimer();
|
||||
this.detachScrollListener();
|
||||
this.detachIntersectionObserver();
|
||||
}
|
||||
|
||||
attachIntersectionObserver () {
|
||||
attachIntersectionObserver() {
|
||||
this.intersectionObserverWrapper.connect();
|
||||
}
|
||||
|
||||
detachIntersectionObserver () {
|
||||
detachIntersectionObserver() {
|
||||
this.intersectionObserverWrapper.disconnect();
|
||||
}
|
||||
|
||||
getFirstChildKey (props) {
|
||||
getFirstChildKey(props) {
|
||||
const { children } = props;
|
||||
let firstChild = children;
|
||||
|
||||
@@ -198,7 +198,7 @@ export default class ScrollableList extends PureComponent {
|
||||
this.props.onLoadMore();
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { children, scrollKey, showLoading, isLoading, hasMore, emptyMessage, onLoadMore } = this.props;
|
||||
const childrenCount = React.Children.count(children);
|
||||
|
||||
@@ -207,28 +207,33 @@ export default class ScrollableList extends PureComponent {
|
||||
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
||||
|
||||
if (showLoading) {
|
||||
return ( <ColumnIndicator type='loading' /> );
|
||||
return <ColumnIndicator type='loading' />
|
||||
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
|
||||
return (
|
||||
<div className='scrollable-list' onMouseMove={this.handleMouseMove}>
|
||||
<div onMouseMove={this.handleMouseMove}>
|
||||
<div role='feed'>
|
||||
{React.Children.map(this.props.children, (child, index) => (
|
||||
<IntersectionObserverArticle
|
||||
key={child.key}
|
||||
id={child.key}
|
||||
index={index}
|
||||
listLength={childrenCount}
|
||||
intersectionObserverWrapper={this.intersectionObserverWrapper}
|
||||
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
|
||||
>
|
||||
{React.cloneElement(child, {
|
||||
getScrollPosition: this.getScrollPosition,
|
||||
updateScrollBottom: this.updateScrollBottom,
|
||||
cachedMediaWidth: this.state.cachedMediaWidth,
|
||||
cacheMediaWidth: this.cacheMediaWidth,
|
||||
})}
|
||||
</IntersectionObserverArticle>
|
||||
))}
|
||||
{
|
||||
!!this.props.children &&
|
||||
React.Children.map(this.props.children, (child, index) => (
|
||||
<IntersectionObserverArticle
|
||||
key={child.key}
|
||||
id={child.key}
|
||||
index={index}
|
||||
listLength={childrenCount}
|
||||
intersectionObserverWrapper={this.intersectionObserverWrapper}
|
||||
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
|
||||
>
|
||||
{
|
||||
React.cloneElement(child, {
|
||||
getScrollPosition: this.getScrollPosition,
|
||||
updateScrollBottom: this.updateScrollBottom,
|
||||
cachedMediaWidth: this.state.cachedMediaWidth,
|
||||
cacheMediaWidth: this.cacheMediaWidth,
|
||||
})
|
||||
}
|
||||
</IntersectionObserverArticle>
|
||||
))
|
||||
}
|
||||
|
||||
{loadMore}
|
||||
</div>
|
||||
@@ -236,7 +241,7 @@ export default class ScrollableList extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
return ( <ColumnIndicator type='error' message={emptyMessage} /> );
|
||||
return <ColumnIndicator type='error' message={emptyMessage} />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ class Status extends ImmutablePureComponent {
|
||||
onReply: PropTypes.func,
|
||||
onShowRevisions: PropTypes.func,
|
||||
onQuote: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onFavorite: PropTypes.func,
|
||||
onRepost: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onEdit: PropTypes.func,
|
||||
onDirect: PropTypes.func,
|
||||
@@ -212,12 +212,12 @@ class Status extends ImmutablePureComponent {
|
||||
this.props.onReply(this._properStatus(), this.context.router.history);
|
||||
};
|
||||
|
||||
handleHotkeyFavourite = () => {
|
||||
this.props.onFavourite(this._properStatus());
|
||||
handleHotkeyFavorite = () => {
|
||||
this.props.onFavorite(this._properStatus());
|
||||
};
|
||||
|
||||
handleHotkeyBoost = e => {
|
||||
this.props.onReblog(this._properStatus(), e);
|
||||
this.props.onRepost(this._properStatus(), e);
|
||||
};
|
||||
|
||||
handleHotkeyMention = e => {
|
||||
@@ -424,7 +424,7 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
const handlers = this.props.muted ? {} : {
|
||||
reply: this.handleHotkeyReply,
|
||||
favourite: this.handleHotkeyFavourite,
|
||||
favorite: this.handleHotkeyFavorite,
|
||||
boost: this.handleHotkeyBoost,
|
||||
mention: this.handleHotkeyMention,
|
||||
open: this.handleHotkeyOpen,
|
||||
|
||||
@@ -63,8 +63,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
onOpenUnauthorizedModal: PropTypes.func.isRequired,
|
||||
onReply: PropTypes.func,
|
||||
onQuote: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onFavorite: PropTypes.func,
|
||||
onRepost: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onMention: PropTypes.func,
|
||||
onMute: PropTypes.func,
|
||||
@@ -101,17 +101,17 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleFavouriteClick = () => {
|
||||
handleFavoriteClick = () => {
|
||||
if (me) {
|
||||
this.props.onFavourite(this.props.status)
|
||||
this.props.onFavorite(this.props.status)
|
||||
} else {
|
||||
this.props.onOpenUnauthorizedModal()
|
||||
}
|
||||
}
|
||||
|
||||
handleReblogClick = e => {
|
||||
handleRepostClick = e => {
|
||||
if (me) {
|
||||
this.props.onReblog(this.props.status, e)
|
||||
this.props.onRepost(this.props.status, e)
|
||||
} else {
|
||||
this.props.onOpenUnauthorizedModal()
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
const reblogCount = status.get('reblogs_count')
|
||||
const reblogTitle = !publicStatus ? formatMessage(messages.cannot_reblog) : formatMessage(messages.reblog)
|
||||
|
||||
const favoriteCount = status.get('favourites_count')
|
||||
const favoriteCount = status.get('favorites_count') // : todo :
|
||||
|
||||
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
|
||||
<IconButton className='status-action-bar-button' title={formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
|
||||
@@ -139,8 +139,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
{
|
||||
title: formatMessage(messages.like),
|
||||
icon: 'like',
|
||||
active: !!status.get('favourited'),
|
||||
onClick: this.handleFavouriteClick,
|
||||
active: !!status.get('favorited'),
|
||||
onClick: this.handleFavoriteClick,
|
||||
},
|
||||
{
|
||||
title: formatMessage(messages.comment),
|
||||
@@ -153,13 +153,13 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
icon: (status.get('visibility') === 'private') ? 'lock' : 'repost',
|
||||
disabled: !publicStatus,
|
||||
active: !!status.get('reblogged'),
|
||||
onClick: this.handleReblogClick,
|
||||
onClick: this.handleRepostClick,
|
||||
},
|
||||
{
|
||||
title: formatMessage(messages.share),
|
||||
icon: 'share',
|
||||
active: false,
|
||||
onClick: this.handleFavouriteClick,
|
||||
onClick: this.handleFavoriteClick,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ export default class StatusHeader extends ImmutablePureComponent {
|
||||
menu.push({ text: formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||
} else {
|
||||
if (status.get('visibility') === 'private') {
|
||||
menu.push({ text: formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
|
||||
menu.push({ text: formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleRepostClick });
|
||||
}
|
||||
}
|
||||
menu.push({ text: formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
|
||||
Reference in New Issue
Block a user