This commit is contained in:
mgabdev 2020-05-02 02:25:55 -04:00
parent e9f01c0b16
commit 196a906cec
62 changed files with 866 additions and 509 deletions

View File

@ -9,7 +9,7 @@ class Api::V1::GifsController < Api::BaseController
def categories
uri = URI('https://api.tenor.com/v1/categories')
theOptions = { :key => "QHFJ0C5EWGBH" }
theOptions = { :key => "TENOR_KEY" }
uri.query = URI.encode_www_form(theOptions)
res = Net::HTTP.get_response(uri)
@ -19,7 +19,7 @@ class Api::V1::GifsController < Api::BaseController
def search
uri = URI('https://api.tenor.com/v1/search')
theOptions = {
:key => "QHFJ0C5EWGBH",
:key => "TENOR_KEY",
:media_filter => "minimal",
:limit => 30,
:q => params[:search],

View File

@ -22,8 +22,10 @@ class Api::V1::StatusesController < Api::BaseController
render json: @status, serializer: REST::StatusSerializer
end
# direct descendants only
def comments
descendants_results = @status.descendants(CONTEXT_LIMIT, current_account)
descendants_results = @status.descendants(CONTEXT_LIMIT, current_account, nil, nil, 1)
puts "descendants_results: " + descendants_results.inspect
loaded_descendants = cache_collection(descendants_results, Status)
@context = Context.new(descendants: loaded_descendants)

View File

@ -1,6 +1,7 @@
import api from '../api';
import { CancelToken, isCancel } from 'axios';
import throttle from 'lodash.throttle'
import moment from 'moment-mini'
import { search as emojiSearch } from '../components/emoji/emoji_mart_search_light';
import { urlRegex } from '../features/ui/util/url_regex'
import { tagHistory } from '../settings';
@ -77,23 +78,26 @@ export const ensureComposeIsVisible = (getState, routerHistory) => {
}
};
export function changeCompose(text, markdown) {
console.log("changeCompose:", markdown)
export function changeCompose(text, markdown, replyId) {
console.log("changeCompose:", text)
return {
type: COMPOSE_CHANGE,
text: text,
markdown: markdown,
replyId: replyId,
};
};
export function replyCompose(status) {
export function replyCompose(status, router, showModal) {
return (dispatch) => {
dispatch({
type: COMPOSE_REPLY,
status: status,
});
dispatch(openModal('COMPOSE'));
if (showModal) {
dispatch(openModal('COMPOSE'));
}
};
};
@ -169,7 +173,7 @@ export function handleComposeSubmit(dispatch, getState, response, status) {
}
}
export function submitCompose(group, replyToId=null) {
export function submitCompose(group, replyToId = null) {
return function (dispatch, getState) {
if (!me) return;
@ -202,8 +206,7 @@ export function submitCompose(group, replyToId=null) {
const method = id === null ? 'post' : 'put';
const scheduled_at = getState().getIn(['compose', 'scheduled_at'], null);
// : todo :
// if (scheduled_at !== null) scheduled_at = moment.utc(scheduled_at).toDate();
if (scheduled_at !== null) scheduled_at = moment.utc(scheduled_at).toDate();
api(getState)[method](endpoint, {
status,

View File

@ -52,9 +52,14 @@ export const clearGifResults = () => ({
type: GIFS_CLEAR_RESULTS,
})
export const setSelectedGif = (url) => ({
export const clearSelectedGif = () => ({
type: GIF_CLEAR_SELECTED,
result,
})
export const setSelectedGif = (result) => ({
type: GIF_SET_SELECTED,
url,
result,
})
export function changeGifSearchText(text) {

View File

@ -16,8 +16,8 @@ const GlobeIcon = ({
aria-label={title}
>
<g>
<circle fill='none' stroke='#616770' strokeWidth='1px' cx='14' cy='14' r='13.5' />
<path fill='#616770' d='M 16 5 L 18 4 L 20 5 L 22 5 L 23 4 C 24 5 25 6 25 7 L 25 7 L 22 8 L 20 7 L 19 6 L 16 6 L 14 7 L 15 11 L 18 12 L 20 12 L 21 13 L 21 14 L 22 16 L 23 18 L 23 20 L 26.4 21 C 20 27 12 29 6 25 C 1 21 0 14 1 8 L 2 11 L 3 12 L 5 13 L 4 14 L 4 15 L 5 17 L 7 17 L 7 22 L 8 24 L 9 25 L 9 22 L 11 21 L 11 20 L 13 18 L 14 15 L 12 15 L 10 13 L 7 13 L 6 11 L 5 13 L 5 11 L 4 10 L 4 8 L 7 8 L 9 7 L 11 4 L 12 4 L 13 2 L 10 2 L 10 1 C 12 0 16 0 18 1 L 18 2 L 16 2 L 15 4 Z M 16 5' />
<circle fill='none' stroke='#4B4F55' strokeWidth='1px' cx='14' cy='14' r='13.5' />
<path fill='#4B4F55' d='M 16 5 L 18 4 L 20 5 L 22 5 L 23 4 C 24 5 25 6 25 7 L 25 7 L 22 8 L 20 7 L 19 6 L 16 6 L 14 7 L 15 11 L 18 12 L 20 12 L 21 13 L 21 14 L 22 16 L 23 18 L 23 20 L 26.4 21 C 20 27 12 29 6 25 C 1 21 0 14 1 8 L 2 11 L 3 12 L 5 13 L 4 14 L 4 15 L 5 17 L 7 17 L 7 22 L 8 24 L 9 25 L 9 22 L 11 21 L 11 20 L 13 18 L 14 15 L 12 15 L 10 13 L 7 13 L 6 11 L 5 13 L 5 11 L 4 10 L 4 8 L 7 8 L 9 7 L 11 4 L 12 4 L 13 2 L 10 2 L 10 1 C 12 0 16 0 18 1 L 18 2 L 16 2 L 15 4 Z M 16 5' />
</g>
</svg>
)

View File

@ -11,12 +11,12 @@ const VerifiedIcon = ({
y='0px'
width={size}
height={size}
viewBox='0 0 24 24'
viewBox='0 0 32 32'
xmlSpace='preserve'
aria-label={title}
>
<g>
<path fill='#3E99ED' d='M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .494.083.964.237 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z' />
<path fill='#3E99ED' d='M 27.3125 4.6875 C 24.292969 1.664062 20.273438 0 16 0 C 11.726562 0 7.707031 1.664062 4.6875 4.6875 C 1.664062 7.707031 0 11.726562 0 16 C 0 20.273438 1.664062 24.292969 4.6875 27.3125 C 7.707031 30.335938 11.726562 32 16 32 C 20.273438 32 24.292969 30.335938 27.3125 27.3125 C 30.335938 24.292969 32 20.273438 32 16 C 32 11.726562 30.335938 7.707031 27.3125 4.6875 Z M 23.644531 12.191406 L 14.703125 21.132812 C 14.519531 21.316406 14.28125 21.410156 14.039062 21.410156 C 13.800781 21.410156 13.558594 21.316406 13.375 21.132812 L 8.355469 16.113281 C 7.988281 15.746094 7.988281 15.152344 8.355469 14.785156 C 8.722656 14.421875 9.316406 14.421875 9.683594 14.785156 L 14.039062 19.144531 L 22.316406 10.867188 C 22.683594 10.5 23.277344 10.5 23.644531 10.867188 C 24.011719 11.230469 24.011719 11.824219 23.644531 12.191406 Z M 23.644531 12.191406' />
</g>
</svg>
)

View File

@ -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>

View File

@ -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>

View File

@ -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>
}
}

View File

@ -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,

View File

@ -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

View File

@ -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) {

View File

@ -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>
);
)
}
}

View File

@ -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'>

View File

@ -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}
/>
{

View File

@ -22,7 +22,7 @@ class LoadMore extends PureComponent {
}
handleClick = (e) => {
this.props.onClick()
this.props.onClick(e)
}
render() {

View File

@ -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}
/>

View File

@ -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,

View File

@ -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>
)
}
}

View File

@ -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>

View File

@ -135,7 +135,7 @@ class ModalRoot extends PureComponent {
}
renderLoading = () => {
return <ModalLoading />
return null
}
renderError = () => {

View File

@ -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)}

View File

@ -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 => {

View File

@ -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(' ')}
>
{

View File

@ -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>
)
}

View File

@ -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

View File

@ -57,12 +57,6 @@ class PopoverRoot extends PureComponent {
props: PropTypes.object,
}
getSnapshotBeforeUpdate() {
return {
visible: !!this.props.type
}
}
renderEmpty = () => {
return <div />
}

View File

@ -197,7 +197,7 @@ export default class ScrollableList extends PureComponent {
}
handleLoadMore = (e) => {
e.preventDefault();
e.preventDefault()
this.props.onLoadMore();
}

View File

@ -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>

View File

@ -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 &&

View File

@ -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 = () => {

View File

@ -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,

View File

@ -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') &&

View 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
}
}

View File

@ -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>
);
)
}
}

View File

@ -1,4 +1,4 @@
import { defineMessages, injectIntl } from 'react-intl'
import { FormattedMessage } from 'react-intl'
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import {
replyCompose,
@ -34,16 +34,8 @@ import {
import { makeGetStatus } from '../selectors';
import Status from '../components/status';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getStatus = makeGetStatus()
const mapStateToProps = (state, props) => {
const statusId = props.id || props.params.statusId
@ -62,9 +54,12 @@ const makeMapStateToProps = () => {
let indent = -1
descendantsIds = descendantsIds.withMutations(mutable => {
const ids = [status.get('id')]
const r = state.getIn(['contexts', 'replies', ids[0]])
console.log("r:", r)
while (ids.length > 0) {
let id = ids.shift();
let id = ids.shift()
const replies = state.getIn(['contexts', 'replies', id])
if (status.get('id') !== id) {
@ -94,22 +89,22 @@ const makeMapStateToProps = () => {
return mapStateToProps
};
const mapDispatchToProps = (dispatch, { intl }) => ({
onReply (status, router) {
const mapDispatchToProps = (dispatch) => ({
onReply (status, router, showModal) {
if (!me) return dispatch(openModal('UNAUTHORIZED'))
dispatch((_, getState) => {
const state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
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));
dispatch(replyCompose(status, router, showModal));
}
});
})
},
onRepost (targetRef, status, e) {
@ -177,8 +172,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(deleteStatus(status.get('id'), history));
} else {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
message: <FormattedMessage id='confirmations.delete.message' defaultMessage='Are you sure you want to delete this status?' />,
confirm: <FormattedMessage id='confirmations.delete.confirm' defaultMessage='Delete' />,
onConfirm: () => dispatch(deleteStatus(status.get('id'), history)),
}));
}

View File

@ -24,6 +24,8 @@ import StatusContainer from '../../../containers/status_container'
import StatusVisibilityButton from './status_visibility_button'
import UploadButton from './media_upload_button'
import UploadForm from './upload_form'
import GifForm from './gif_form'
import Input from '../../../components/input'
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: "What's on your mind?" },
@ -78,6 +80,7 @@ class ComposeForm extends ImmutablePureComponent {
replyToId: PropTypes.string,
reduxReplyToId: PropTypes.string,
hasPoll: PropTypes.bool,
selectedGifSrc: PropTypes.string,
};
static defaultProps = {
@ -85,7 +88,7 @@ class ComposeForm extends ImmutablePureComponent {
};
handleChange = (e, markdown) => {
this.props.onChange(e.target.value, markdown);
this.props.onChange(e.target.value, markdown, this.props.replyToId)
}
handleComposeFocus = () => {
@ -156,8 +159,8 @@ class ComposeForm extends ImmutablePureComponent {
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
}
handleChangeSpoilerText = (e) => {
this.props.onChangeSpoilerText(e.target.value);
handleChangeSpoilerText = (value) => {
this.props.onChangeSpoilerText(value)
}
componentDidMount() {
@ -169,7 +172,7 @@ class ComposeForm extends ImmutablePureComponent {
}
componentDidUpdate(prevProps) {
if (!this.autosuggestTextarea) return;
if (!this.autosuggestTextarea) return
// This statement does several things:
// - If we're beginning a reply, and,
@ -196,17 +199,13 @@ class ComposeForm extends ImmutablePureComponent {
}
setAutosuggestTextarea = (c) => {
this.autosuggestTextarea = c;
this.autosuggestTextarea = c
}
setForm = (c) => {
this.form = c
}
setSpoilerText = (c) => {
this.spoilerText = c
}
handleEmojiPick = (data) => {
const { text } = this.props
const position = this.autosuggestTextarea.textbox.selectionStart
@ -236,12 +235,13 @@ class ComposeForm extends ImmutablePureComponent {
isMatch,
isChangingUpload,
isSubmitting,
selectedGifSrc,
} = this.props
const disabled = isSubmitting
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
const disabledButton = disabled || isUploading || isChangingUpload || length(text) > MAX_POST_CHARACTER_COUNT || (length(text) !== 0 && length(text.trim()) === 0 && !anyMedia);
const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth)
const parentContainerClasses = CX({
default: 1,
width100PC: 1,
@ -294,7 +294,7 @@ class ComposeForm extends ImmutablePureComponent {
>
{
!!reduxReplyToId && !shouldCondense &&
!!reduxReplyToId && !shouldCondense && isModalOpen &&
<div className={[_s.default, _s.px15, _s.py10, _s.mt5].join(' ')}>
<StatusContainer
id={reduxReplyToId}
@ -306,19 +306,13 @@ class ComposeForm extends ImmutablePureComponent {
{
!!spoiler &&
<div className={[_s.default, _s.px15, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
<AutosuggestTextbox
<Input
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}
onChange={this.handleChangeSpoilerText}
onKeyDown={this.handleKeyDown}
disabled={!this.props.spoiler}
ref={this.setSpoilerText}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSpoilerSuggestionSelected}
searchTokens={[':']}
prependIcon='warning'
maxLength={256}
id='cw-spoiler-input'
/>
</div>
@ -349,12 +343,17 @@ class ComposeForm extends ImmutablePureComponent {
</div>
}
{ /* : todo : for gif
(isUploading || hasGif) &&
{
/*
!!selectedGifSrc && !anyMedia &&
<div className={[_s.default, _s.px15].join(' ')}>
<UploadForm replyToId={replyToId} />
<GifForm
replyToId={replyToId}
small={shouldCondense}
selectedGifSrc={selectedGifSrc}
/>
</div>
*/
*/
}
{
@ -365,7 +364,7 @@ class ComposeForm extends ImmutablePureComponent {
}
{
!!quoteOfId &&
!!quoteOfId && isModalOpen &&
<div className={[_s.default, _s.px15, _s.py10, _s.mt5].join(' ')}>
<StatusContainer
id={quoteOfId}
@ -376,29 +375,21 @@ class ComposeForm extends ImmutablePureComponent {
<div className={actionsContainerClasses}>
<div className={[_s.default, _s.flexRow, _s.mrAuto].join(' ')}>
{
!shouldCondense &&
<RichTextEditorButton />
}
<EmojiPickerButton small={shouldCondense} isMatch={isMatch} />
<UploadButton small={shouldCondense} />
{ /* <GifSelectorButton small={shouldCondense} /> */ }
{
!edit && !shouldCondense &&
<PollButton />
}
{
!shouldCondense &&
<StatusVisibilityButton />
}
{
!shouldCondense &&
<SpoilerButton />
}
{
!shouldCondense &&
<SchedulePostButton />
}
<GifSelectorButton small={shouldCondense} />
<EmojiPickerButton small={shouldCondense} isMatch={isMatch} />
{ !shouldCondense && <StatusVisibilityButton /> }
{ !shouldCondense && <SpoilerButton /> }
{ !shouldCondense && <SchedulePostButton /> }
{ /* !shouldCondense && <RichTextEditorButton /> */ }
{
shouldCondense &&

View File

@ -1,36 +1,39 @@
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ProgressBar from '../../../../components/progress_bar'
import Upload from '../media_upload_item'
import SensitiveMediaButton from '../sensitive_media_button'
import { clearSelectedGif } from '../../../actions/tenor'
import Image from '../../../components/image'
const mapStateToProps = (state) => ({
mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')),
isUploading: state.getIn(['compose', 'is_uploading']),
uploadProgress: state.getIn(['compose', 'progress']),
});
const mapDispatchToProps = (dispatch) => ({
onClearSelectedGif() {
dispatch(clearSelectedGif())
},
})
export default
@connect(mapStateToProps)
class GifForm extends ImmutablePureComponent {
@connect(null, mapDispatchToProps)
class GifForm extends PureComponent {
static propTypes = {
mediaIds: ImmutablePropTypes.list.isRequired,
isUploading: PropTypes.bool,
uploadProgress: PropTypes.number,
};
onClearSelectedGif: PropTypes.func.isRequired,
replyToId: PropTypes.string,
small: PropTypes.bool,
selectedGifSrc: PropTypes.string.isRequired,
}
render () {
const {
mediaIds,
isUploading,
uploadProgress,
selectedGifSrc,
small,
} = this.props
if (!selectedGifSrc) return null
return (
<div className={_s.default}>
<div className={[_s.default, _s.flexRow, _s.flexWrap].join(' ')}>
<Upload id={id} key={id} />
<Image
width='auto'
src={selectedGifSrc}
className={[_s.maxWidth100PC, _s.radiusSmall, _s.height260PX].join(' ')}
/>
</div>
</div>
)

View File

@ -12,7 +12,7 @@ const messages = defineMessages({
const makeMapStateToProps = () => {
const mapStateToProps = (state) => ({
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio', 'gifv'].includes(m.get('type')))),
unavailable: state.getIn(['compose', 'poll']) !== null,
resetFileKey: state.getIn(['compose', 'resetFileKey']),
})

View File

@ -7,6 +7,7 @@ import { submitCompose } from '../../../actions/compose';
import Button from '../../../components/button'
import Image from '../../../components/image'
import Input from '../../../components/input'
import Text from '../../../components/text'
const cx = classNames.bind(_s)
@ -15,27 +16,20 @@ const messages = defineMessages({
delete: { id: 'upload_form.undo', defaultMessage: 'Delete' },
})
const mapStateToProps = (state, { id, otherProps }) => {
console.log("otherProps:", otherProps)
return {
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
}
}
const mapStateToProps = (state, { id }) => ({
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
})
const mapDispatchToProps = (dispatch) => ({
onUndo: id => {
dispatch(undoUploadCompose(id));
onUndo: (id) => {
dispatch(undoUploadCompose(id))
},
onDescriptionChange: (id, description) => {
dispatch(changeUploadCompose(id, { description }));
dispatch(changeUploadCompose(id, { description }))
},
onSubmit () {
dispatch(submitCompose());
dispatch(submitCompose())
},
});
export default
@ -72,13 +66,13 @@ class Upload extends ImmutablePureComponent {
this.props.onSubmit()
}
handleUndoClick = e => {
handleUndoClick = (e) => {
e.stopPropagation()
this.props.onUndo(this.props.media.get('id'))
}
handleInputChange = e => {
this.setState({ dirtyDescription: e.target.value })
handleInputChange = (value) => {
this.setState({ dirtyDescription: value })
}
handleMouseEnter = () => {
@ -128,8 +122,6 @@ class Upload extends ImmutablePureComponent {
displayNone: !active,
})
console.log("media:", media)
return (
<div
tabIndex='0'
@ -144,6 +136,12 @@ class Upload extends ImmutablePureComponent {
className={[_s.default, _s.height158PX].join(' ')}
src={media.get('preview_url')}
/>
{
media.get('type') === 'gifv' &&
<div className={[_s.default, _s.posAbs, _s.z2, _s.radiusSmall, _s.bgBlackOpaque, _s.px5, _s.py5, _s.ml10, _s.mt10, _s.top0, _s.left0].join(' ')}>
<Text size='extraSmall' color='white' weight='medium'>GIF</Text>
</div>
}
<Button
backgroundColor='black'
color='white'

View File

@ -7,14 +7,11 @@ import {
removePollOption,
changePollOption,
changePollSettings,
clearComposeSuggestions,
fetchComposeSuggestions,
selectComposeSuggestion,
} from '../../../actions/compose'
import Button from '../../../components/button'
import Text from '../../../components/text'
import Select from '../../../components/select'
import AutosuggestTextbox from '../../../components/autosuggest_textbox'
import Input from '../../../components/input'
const cx = classNames.bind(_s)
@ -29,7 +26,6 @@ const messages = defineMessages({
})
const mapStateToProps = (state) => ({
suggestions: state.getIn(['compose', 'suggestions']),
options: state.getIn(['compose', 'poll', 'options']),
expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
isMultiple: state.getIn(['compose', 'poll', 'multiple']),
@ -52,18 +48,6 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(changePollSettings(expiresIn, isMultiple))
},
onClearSuggestions () {
dispatch(clearComposeSuggestions())
},
onFetchSuggestions (token) {
dispatch(fetchComposeSuggestions(token))
},
onSuggestionSelected (position, token, accountId, path) {
dispatch(selectComposeSuggestion(position, token, accountId, path))
},
})
export default
@ -79,10 +63,6 @@ class PollForm extends ImmutablePureComponent {
onAddOption: PropTypes.func.isRequired,
onRemoveOption: PropTypes.func.isRequired,
onChangeSettings: PropTypes.func.isRequired,
suggestions: ImmutablePropTypes.list,
onClearSuggestions: PropTypes.func.isRequired,
onFetchSuggestions: PropTypes.func.isRequired,
onSuggestionSelected: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
}
@ -200,15 +180,11 @@ class PollFormOption extends ImmutablePureComponent {
onChange: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
onToggleMultiple: PropTypes.func.isRequired,
suggestions: ImmutablePropTypes.list,
onClearSuggestions: PropTypes.func.isRequired,
onFetchSuggestions: PropTypes.func.isRequired,
onSuggestionSelected: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
}
handleOptionTitleChange = e => {
this.props.onChange(this.props.index, e.target.value)
handleOptionTitleChange = (value) => {
this.props.onChange(this.props.index, value)
}
handleOptionRemove = () => {
@ -221,18 +197,6 @@ class PollFormOption extends ImmutablePureComponent {
e.stopPropagation()
}
onSuggestionsClearRequested = () => {
this.props.onClearSuggestions()
}
onSuggestionsFetchRequested = (token) => {
this.props.onFetchSuggestions(token)
}
onSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index])
}
render() {
const { isPollMultiple, title, index, intl } = this.props
@ -257,16 +221,11 @@ class PollFormOption extends ImmutablePureComponent {
tabIndex='0'
/>
<AutosuggestTextbox
<Input
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
maxLength={25}
maxLength={64}
value={title}
onChange={this.handleOptionTitleChange}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
searchTokens={[':']}
/>
</label>

View File

@ -7,7 +7,6 @@ import {
fetchComposeSuggestions,
selectComposeSuggestion,
changeComposeSpoilerText,
insertEmojiCompose,
uploadCompose,
changeScheduledAt,
} from '../../../actions/compose'
@ -18,35 +17,66 @@ const mapStateToProps = (state, { replyToId }) => {
const reduxReplyToId = state.getIn(['compose', 'in_reply_to'])
const isMatch = reduxReplyToId || replyToId ? reduxReplyToId === replyToId : true
if (!isMatch) {
return {
isMatch,
edit: null,
text: '',
suggestions: ImmutableList(),
spoiler: false,
spoilerText: '',
privacy: null,
focusDate: null,
caretPosition: null,
preselectDate: null,
isSubmitting: false,
isChangingUpload: false,
isUploading: false,
showSearch: false,
anyMedia: false,
isModalOpen: false,
quoteOfId: null,
scheduledAt: null,
account: state.getIn(['accounts', me]),
hasPoll: false,
selectedGifSrc: null,
reduxReplyToId
}
}
// console.log("isMatch:", isMatch, reduxReplyToId, replyToId, state.getIn(['compose', 'text']))
return {
isMatch,
edit: !isMatch ? null : state.getIn(['compose', 'id']) !== null,
text: !isMatch ? '' : state.getIn(['compose', 'text']),
suggestions: !isMatch ? ImmutableList() : state.getIn(['compose', 'suggestions']),
spoiler: !isMatch ? false : state.getIn(['compose', 'spoiler']),
spoilerText: !isMatch ? '' : state.getIn(['compose', 'spoiler_text']),
privacy: !isMatch ? null : state.getIn(['compose', 'privacy']),
focusDate: !isMatch ? null : state.getIn(['compose', 'focusDate']),
caretPosition: !isMatch ? null : state.getIn(['compose', 'caretPosition']),
preselectDate: !isMatch ? null : state.getIn(['compose', 'preselectDate']),
isSubmitting: !isMatch ? false : state.getIn(['compose', 'is_submitting']),
isChangingUpload: !isMatch ? false : state.getIn(['compose', 'is_changing_upload']),
isUploading: !isMatch ? false : state.getIn(['compose', 'is_uploading']),
showSearch: !isMatch ? false : state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: !isMatch ? false : state.getIn(['compose', 'media_attachments']).size > 0,
isModalOpen: !isMatch ? false : state.getIn(['modal', 'modalType']) === 'COMPOSE',
quoteOfId: !isMatch ? null : state.getIn(['compose', 'quote_of_id']),
scheduledAt: !isMatch ? null : state.getIn(['compose', 'scheduled_at']),
edit: state.getIn(['compose', 'id']) !== null,
text: state.getIn(['compose', 'text']),
suggestions: state.getIn(['compose', 'suggestions']),
spoiler: state.getIn(['compose', 'spoiler']),
spoilerText: state.getIn(['compose', 'spoiler_text']),
privacy: state.getIn(['compose', 'privacy']),
focusDate: state.getIn(['compose', 'focusDate']),
caretPosition: state.getIn(['compose', 'caretPosition']),
preselectDate: state.getIn(['compose', 'preselectDate']),
isSubmitting: state.getIn(['compose', 'is_submitting']),
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
isUploading: state.getIn(['compose', 'is_uploading']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
isModalOpen: state.getIn(['modal', 'modalType']) === 'COMPOSE',
quoteOfId: state.getIn(['compose', 'quote_of_id']),
scheduledAt: state.getIn(['compose', 'scheduled_at']),
account: state.getIn(['accounts', me]),
hasPoll: !isMatch ? false : state.getIn(['compose', 'poll']),
hasPoll: state.getIn(['compose', 'poll']),
selectedGifSrc: state.getIn(['tenor', 'selectedGif', 'src']),
reduxReplyToId,
}
}
const mapDispatchToProps = (dispatch) => ({
const mapDispatchToProps = (dispatch, { reduxReplyToId, replyToId }) => ({
onChange(text, markdown) {
dispatch(changeCompose(text, markdown))
onChange(text, markdown, newReplyToId) {
console.log("text:", text, newReplyToId, replyToId, reduxReplyToId)
dispatch(changeCompose(text, markdown, newReplyToId))
},
onSubmit(group, replyToId) {
@ -73,10 +103,6 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(uploadCompose(files))
},
onPickEmoji(position, data, needsSpace) {
dispatch(insertEmojiCompose(position, data, needsSpace))
},
setScheduledAt(date) {
dispatch(changeScheduledAt(date))
},

View File

@ -30,8 +30,8 @@ class ListEditorSearch extends PureComponent {
onClear: PropTypes.func.isRequired,
};
handleChange = e => {
this.props.onChange(e.target.value);
handleChange = (value) => {
this.props.onChange(value);
}
handleKeyUp = e => {

View File

@ -54,7 +54,7 @@ class Search extends ImmutablePureComponent {
const showHashtags = pathname === '/search/hashtags'
const showGroups = pathname === '/search/groups'
const isTop = !showPeople && !showHashtags && !showGroups
const theLimit = 10
const theLimit = 4
let accounts, statuses, hashtags, groups

View File

@ -345,7 +345,7 @@
"status.report": "Report @{name}",
"status.sensitive_warning": "Деликатно съдържание",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_less": "Hide",
"status.show_less_all": "Show less for all",
"status.show_more": "Show more",
"status.show_more_all": "Show more for all",

View File

@ -379,7 +379,7 @@
"id": "status.show_more"
},
{
"defaultMessage": "Show less",
"defaultMessage": "Hide",
"id": "status.show_less"
}
],

View File

@ -348,7 +348,7 @@
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_less": "Hide",
"status.show_less_all": "Show less for all",
"status.show_more": "Show more",
"status.show_more_all": "Show more for all",

View File

@ -345,7 +345,7 @@
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_less": "Hide",
"status.show_less_all": "Show less for all",
"status.show_more": "Show more",
"status.show_more_all": "Show more for all",

View File

@ -345,7 +345,7 @@
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_less": "Hide",
"status.show_less_all": "Show less for all",
"status.show_more": "Show more",
"status.show_more_all": "Show more for all",

View File

@ -345,7 +345,7 @@
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_less": "Hide",
"status.show_less_all": "Show less for all",
"status.show_more": "Show more",
"status.show_more_all": "Show more for all",

View File

@ -345,7 +345,7 @@
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_less": "Hide",
"status.show_less_all": "Show less for all",
"status.show_more": "Show more",
"status.show_more_all": "Show more for all",

View File

@ -170,11 +170,13 @@ const updateSuggestionTags = (state, token) => {
};
const insertEmoji = (state, position, emojiData, needsSpace) => {
const oldText = state.get('text');
const emoji = needsSpace ? ' ' + emojiData.native : emojiData.native;
const oldText = state.get('text')
const emoji = needsSpace ? ' ' + emojiData.native : emojiData.native
const text = `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`
console.log("insertEmoji reducer:", emoji, position, emojiData, needsSpace, text)
return state.merge({
text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`,
text,
focusDate: new Date(),
caretPosition: position + emoji.length + 1,
idempotencyKey: uuid(),
@ -248,10 +250,14 @@ export default function compose(state = initialState, action) {
.set('privacy', action.value)
.set('idempotencyKey', uuid());
case COMPOSE_CHANGE:
return state
.set('text', action.text)
.set('markdown', action.markdown)
.set('idempotencyKey', uuid());
return state.withMutations(map => {
map.set('text', action.text)
map.set('markdown', action.markdown)
map.set('idempotencyKey', uuid())
if (action.replyId) {
map.set('in_reply_to', action.replyId)
}
})
case COMPOSE_COMPOSING_CHANGE:
return state.set('is_composing', action.value);
case COMPOSE_REPLY:

View File

@ -1,6 +1,7 @@
import {
GIFS_CLEAR_RESULTS,
GIF_SET_SELECTED,
GIF_CLEAR_SELECTED,
GIF_CHANGE_SEARCH_TEXT,
GIF_RESULTS_FETCH_REQUEST,
GIF_RESULTS_FETCH_SUCCESS,
@ -14,11 +15,15 @@ import { Map as ImmutableMap } from 'immutable'
const initialState = ImmutableMap({
categories: [],
results: [],
chosenUrl: '',
searchText: '',
next: 0,
loading: false,
error: false,
selectedGif: ImmutableMap({
id: '',
title: '',
src: '',
})
})
export default function (state = initialState, action) {
@ -48,7 +53,13 @@ export default function (state = initialState, action) {
case GIFS_CLEAR_RESULTS:
return state.set('results', [])
case GIF_SET_SELECTED:
return state.set('chosenUrl', action.url)
return state.set('selectedGif', ImmutableMap({
id: action.result.id,
title: action.result.title,
src: action.result.media[0].gif.url,
}))
case GIF_CLEAR_SELECTED:
return state.set('selectedGif', ImmutableMap())
case GIF_CHANGE_SEARCH_TEXT:
return state.set('searchText', action.text.trim());
default:

View File

@ -7,7 +7,7 @@
--color_white: #fff;
--color_black: #2d3436;
--color_black-opaque: rgba(0, 0, 0, .8);
--color_black-opaquer: rgba(0, 0, 0, .5);
--color_black-opaquer: rgba(0, 0, 0, .65);
--color_gold: #ffd700;
--color_red: #de2960;
--color_red-dark: #c72c5b;
@ -99,6 +99,7 @@ body {
margin-top: 0;
margin-bottom: 0;
font-size: var(--fs_m);
line-height: 1.3125;
overflow-wrap: break-word;
color: var(--text_color_primary);
font-family: system-ui, -apple-system, BlinkMacSystemFont, Roboto, Ubuntu, "Helvetica Neue", sans-serif;
@ -140,6 +141,7 @@ body {
margin-top: 0;
margin-bottom: 0;
font-size: var(--fs_n);
line-height: 1.3125;
overflow-wrap: break-word;
color: var(--text_color_primary);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif;
@ -384,6 +386,7 @@ body {
/* */
.lineHeight0825 { line-height: 0.825em; }
.lineHeight125 { line-height: 1.25em; }
.lineHeight15 { line-height: 1.5em; }
.lineHeight2 { line-height: 2em; }
@ -402,12 +405,15 @@ body {
}
.heightMax100VH { max-height: 100vh; }
.heightMax100PC { max-height: 100%; }
.heightMax80VH { max-height: 80vh; }
.heightMax200PX { max-height: 200px; }
.heightMax56PX { max-height: 56px; }
.heightCalc53PX { height: calc(100vh - 53px); }
.heightMin100VH { min-height: 100vh; }
.heightMin50VH { min-height: 50vh; }
.heightMin100PX { min-height: 100px; }
.heightMin50PX { min-height: 50px; }
.height100PC { height: 100%; }
@ -422,8 +428,10 @@ body {
.height24PX { height: 24px; }
.height22PX { height: 22px; }
.height20PX { height: 20px; }
.height10PX { height: 10px; }
.height4PX { height: 4px; }
.height1PX { height: 1px; }
.heightAuto { height: auto; }
.maxWidth100PC { max-width: 100%; }
.maxWidth640PX { max-width: 640px; }
@ -442,9 +450,10 @@ body {
.width72PX { width: 72px; }
.width50PX { width: 50px; }
.width20PX { width: 20px; }
.width10PX { width: 10px; }
.width4PX { width: 4px; }
.width1PX { width: 1px; }
.widthAuto { width: auto; }
@media (min-width: 1480px) {
.width1015PX {
@ -561,9 +570,8 @@ body {
.underline { text-decoration: underline; }
.underline_onHover:hover { text-decoration: underline; }
.objectFitCover {
object-fit: cover;
}
.objectFitCover { object-fit: cover; }
.objectFitContain { object-fit: contain; }
.textShadow {
text-shadow: 0 0 5px var(--color_black);

View File

@ -83,7 +83,8 @@ class EditStatusService < BaseService
@media = @account.media_attachments.where(id: @options[:media_ids].take(4).map(&:to_i))
raise GabSocial::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:video?)
hasVideoOrGif = @media.find(&:video?) || @media.find(&:gifv?)
raise GabSocial::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && hasVideoOrGif
end
def language_from_option(str)

View File

@ -3,7 +3,7 @@
class HashtagQueryService < BaseService
LIMIT_PER_MODE = 1
def call(tag, params, account = nil, local = false)
def call(tag, params, account = nil, local = true)
tags = tags_for(Array(tag.name) | Array(params[:any])).pluck(:id)
all = tags_for(params[:all])
none = tags_for(params[:none])

View File

@ -115,7 +115,8 @@ class PostStatusService < BaseService
@media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i))
raise GabSocial::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:video?)
hasVideoOrGif = @media.find(&:video?) || @media.find(&:gifv?)
raise GabSocial::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && hasVideoOrGif
end
def language_from_option(str)

View File

@ -724,7 +724,7 @@ en:
limit: You have reached the maximum amount of lists
media_attachments:
validations:
images_and_video: Cannot attach a video to a status that already contains images
images_and_video: Cannot attach a video or gif to a status that already contains images
too_many: Cannot attach more than 4 files
migrations:
acct: username@domain of the new account

View File

@ -685,7 +685,7 @@ en_GB:
limit: You have reached the maximum amount of lists
media_attachments:
validations:
images_and_video: Cannot attach a video to a status that already contains images
images_and_video: Cannot attach a video or gif to a status that already contains images
too_many: Cannot attach more than 4 files
migrations:
acct: username@domain of the new account

View File

@ -181,7 +181,7 @@ io:
upload: Kargar
media_attachments:
validations:
images_and_video: Cannot attach a video to a status that already contains images
images_and_video: Cannot attach a video or gif to a status that already contains images
too_many: Cannot attach more than 4 files
notification_mailer:
digest:

View File

@ -118,6 +118,7 @@
"markdown-draft-js": "^2.2.0",
"mini-css-extract-plugin": "^0.9.0",
"mkdirp": "^0.5.1",
"moment-mini": "^2.24.0",
"npmlog": "^4.1.2",
"object-assign": "^4.1.1",
"object-fit-images": "^3.2.3",
@ -145,6 +146,7 @@
"react-router-scroll-4": "^1.0.0-beta.1",
"react-stickynode": "^2.1.1",
"react-swipeable-views": "^0.13.0",
"react-textarea-autosize": "^7.1.2",
"redis": "^2.7.1",
"redux": "^4.0.1",
"redux-immutable": "^4.0.0",

View File

@ -6296,6 +6296,11 @@ mkdirp@^0.5, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1:
dependencies:
minimist "^1.2.5"
moment-mini@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment-mini/-/moment-mini-2.24.0.tgz#fa68d98f7fe93ae65bf1262f6abb5fb6983d8d18"
integrity sha512-9ARkWHBs+6YJIvrIp0Ik5tyTTtP9PoV0Ssu2Ocq5y9v8+NOOpWiRshAp8c4rZVWTOe+157on/5G+zj5pwIQFEQ==
moment-timezone@^0.5.x:
version "0.5.28"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.28.tgz#f093d789d091ed7b055d82aa81a82467f72e4338"
@ -8019,6 +8024,14 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.7.0:
react-is "^16.8.6"
scheduler "^0.19.1"
react-textarea-autosize@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-7.1.2.tgz#70fdb333ef86bcca72717e25e623e90c336e2cda"
integrity sha512-uH3ORCsCa3C6LHxExExhF4jHoXYCQwE5oECmrRsunlspaDAbS4mGKNlWZqjLfInWtFQcf0o1n1jC/NGXFdUBCg==
dependencies:
"@babel/runtime" "^7.1.2"
prop-types "^15.6.0"
react@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"