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

View File

@ -22,8 +22,10 @@ class Api::V1::StatusesController < Api::BaseController
render json: @status, serializer: REST::StatusSerializer render json: @status, serializer: REST::StatusSerializer
end end
# direct descendants only
def comments 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) loaded_descendants = cache_collection(descendants_results, Status)
@context = Context.new(descendants: loaded_descendants) @context = Context.new(descendants: loaded_descendants)

View File

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

View File

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

View File

@ -16,8 +16,8 @@ const GlobeIcon = ({
aria-label={title} aria-label={title}
> >
<g> <g>
<circle fill='none' stroke='#616770' strokeWidth='1px' cx='14' cy='14' r='13.5' /> <circle fill='none' stroke='#4B4F55' 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' /> <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> </g>
</svg> </svg>
) )

View File

@ -11,12 +11,12 @@ const VerifiedIcon = ({
y='0px' y='0px'
width={size} width={size}
height={size} height={size}
viewBox='0 0 24 24' viewBox='0 0 32 32'
xmlSpace='preserve' xmlSpace='preserve'
aria-label={title} aria-label={title}
> >
<g> <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> </g>
</svg> </svg>
) )

View File

@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'
import isObject from 'lodash.isobject' import isObject from 'lodash.isobject'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePureComponent from 'react-immutable-pure-component'
import Textarea from 'react-textarea-autosize'
import { isRtl } from '../utils/rtl' import { isRtl } from '../utils/rtl'
import { textAtCursorMatchesToken } from '../utils/cursor_token_match' import { textAtCursorMatchesToken } from '../utils/cursor_token_match'
import AutosuggestAccount from './autosuggest_account' import AutosuggestAccount from './autosuggest_account'
@ -226,27 +227,56 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
direction: isRtl(value) ? 'rtl' : 'ltr', direction: isRtl(value) ? 'rtl' : 'ltr',
} }
const textClasses = cx({ const textareaClasses = cx({
default: 1, default: 1,
lineHeight125: 1, font: 1,
wrap: 1,
resizeNone: 1, resizeNone: 1,
text: 1, bgTransparent: 1,
displayBlock: 1,
outlineNone: 1, outlineNone: 1,
bgPrimary: !small, lineHeight125: 1,
bgSubtle: small, height100PC: small,
py15: !small, width100PC: !small,
py10: small, pt15: !small,
px15: !small,
px10: small,
pb10: !small,
fs16PX: !small, fs16PX: !small,
fs14PX: 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) { if (textarea) {
return ( return (
<Fragment> <Fragment>
<div className={[_s.default, _s.flexGrow1, _s.maxWidth100PC].join(' ')}> <div className={textareaContainerClasses}>
<Composer <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} inputRef={this.setTextbox}
disabled={disabled} disabled={disabled}
placeholder={placeholder} placeholder={placeholder}
@ -259,7 +289,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
onBlur={this.onBlur} onBlur={this.onBlur}
onPaste={this.onPaste} onPaste={this.onPaste}
small={small} small={small}
/> />*/}
{children} {children}
</div> </div>

View File

@ -1,52 +1,125 @@
import { NavLink } from 'react-router-dom' 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 ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component' 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 Avatar from './avatar'
import Button from './button' import Button from './button'
import CommentHeader from './comment_header' import CommentHeader from './comment_header'
import StatusContent from './status_content' import StatusContent from './status_content'
import StatusMedia from './status_media'
import { defaultMediaVisibility } from './status'
import Text from './text' import Text from './text'
const messages = defineMessages({ const messages = defineMessages({
reply: { id: 'status.reply', defaultMessage: 'Reply' }, reply: { id: 'status.reply', defaultMessage: 'Reply' },
like: { id: 'status.like', defaultMessage: 'Like' }, like: { id: 'status.like', defaultMessage: 'Like' },
unlike: { id: 'status.unlike', defaultMessage: 'Unlike' },
}) })
const makeMapStateToProps = (state, props) => ({ const makeMapStateToProps = (state, props) => ({
status: makeGetStatus()(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 export default
@injectIntl @injectIntl
@connect(makeMapStateToProps) @connect(makeMapStateToProps, mapDispatchToProps)
class Comment extends ImmutablePureComponent { class Comment extends ImmutablePureComponent {
static propTypes = { static propTypes = {
indent: PropTypes.number, indent: PropTypes.number,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
isHidden: PropTypes.bool,
isIntersecting: PropTypes.bool,
onReply: PropTypes.func.isRequired,
onFavorite: PropTypes.func.isRequired,
onOpenStatusOptions: PropTypes.func.isRequired,
} }
updateOnProps = [ updateOnProps = [
'status', 'status',
'indent', '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() { render() {
const { const {
indent, indent,
intl, intl,
status, status,
isHidden,
} = this.props } = this.props
if (isHidden) {
return (
<div tabIndex='0'>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')}
</div>
)
}
const style = { const style = {
paddingLeft: `${indent * 40}px`, paddingLeft: `${indent * 40}px`,
} }
// : todo : add media
return ( return (
<div className={[_s.default, _s.px15, _s.mb10, _s.py5].join(' ')} data-comment={status.get('id')}> <div className={[_s.default, _s.px15, _s.mb10, _s.py5].join(' ')} data-comment={status.get('id')}>
<div className={[_s.default].join(' ')} style={style}> <div className={[_s.default].join(' ')} style={style}>
@ -69,12 +142,34 @@ class Comment extends ImmutablePureComponent {
isComment isComment
collapsable 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>
<div className={[_s.default, _s.flexRow, _s.mt5].join(' ')}> <div className={[_s.default, _s.flexRow, _s.mt5].join(' ')}>
<CommentButton title={intl.formatMessage(messages.like)} /> <CommentButton
<CommentButton title={intl.formatMessage(messages.reply)} /> title={intl.formatMessage(status.get('favourited') ? messages.unlike: messages.like)}
<CommentButton title='···' /> onClick={this.handleOnFavorite}
/>
<CommentButton
title={intl.formatMessage(messages.reply)}
onClick={this.handleOnReply}
/>
<CommentButton
title='···'
onClick={this.handleOnOpenStatusOptions}
/>
</div> </div>
</div> </div>

View File

@ -2,7 +2,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePureComponent from 'react-immutable-pure-component'
import Button from './button' import Button from './button'
import Comment from './comment' import Comment from './comment'
import ScrollableList from './scrollable_list'
import Text from './text' import Text from './text'
import { PureComponent } from 'react';
export default class CommentList extends ImmutablePureComponent { export default class CommentList extends ImmutablePureComponent {
@ -11,18 +13,26 @@ export default class CommentList extends ImmutablePureComponent {
descendants: ImmutablePropTypes.list, descendants: ImmutablePropTypes.list,
} }
handleLoadMore = () => {
//
}
render() { render() {
const { const {
descendants, descendants,
commentsLimited, commentsLimited,
} = this.props } = this.props
const upperLimit = 6
const size = descendants.size const size = descendants.size
const max = Math.min(commentsLimited ? 2 : 6, size) const max = Math.min(commentsLimited ? 2 : upperLimit, size)
console.log("max:", size, max)
const Wrapper = !commentsLimited ? ScrollableList : DummyContainer
console.log("Wrapper:", Wrapper)
return ( return (
<div> <div>
<Wrapper scrollKey='comments'>
{ {
descendants.slice(0, max).map((descendant, i) => ( descendants.slice(0, max).map((descendant, i) => (
<Comment <Comment
@ -32,13 +42,15 @@ export default class CommentList extends ImmutablePureComponent {
/> />
)) ))
} }
</Wrapper>
{ {
size > 0 && size > max && size > 0 && size > max && commentsLimited &&
<div className={[_s.default, _s.flexRow, _s.px15, _s.pb5, _s.mb10, _s.alignItemsCenter].join(' ')}> <div className={[_s.default, _s.flexRow, _s.px15, _s.pb5, _s.mb10, _s.alignItemsCenter].join(' ')}>
<Button <Button
isText isText
backgroundColor='none' backgroundColor='none'
color='tertiary' color='tertiary'
onClick={this.handleLoadMore}
> >
<Text weight='bold' color='inherit'> <Text weight='bold' color='inherit'>
View more comments 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,9 +100,23 @@ class Composer extends PureComponent {
} }
state = { state = {
markdownText: '',
plainText: '',
editorState: EditorState.createEmpty(compositeDecorator), 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) => { onChange = (editorState) => {
this.setState({ editorState }) this.setState({ editorState })
const content = this.state.editorState.getCurrentContent(); const content = this.state.editorState.getCurrentContent();
@ -123,15 +137,14 @@ class Composer extends PureComponent {
this.props.onChange(null, text, selectionStart, markdownString) this.props.onChange(null, text, selectionStart, markdownString)
} }
// **bold** // **bold**
// *italic* // *italic*
// __underline__ // __underline__
// ~strikethrough~ // ~strikethrough~
// # title // # title
// > quote // > quote
// `code` // `code`
// ```code``` // ```code```
focus = () => { focus = () => {
this.textbox.editor.focus() this.textbox.editor.focus()
@ -168,7 +181,7 @@ class Composer extends PureComponent {
disabled, disabled,
placeholder, placeholder,
autoFocus, autoFocus,
// value, value,
onChange, onChange,
onKeyDown, onKeyDown,
onKeyUp, onKeyUp,

View File

@ -122,7 +122,7 @@ class DisplayName extends ImmutablePureComponent {
const iconSize = const iconSize =
!!isLarge ? '19px' : !!isLarge ? '19px' :
!!isSmall ? '14px' : '16px' !!isSmall ? '14px' : '15px'
const domain = account.get('acct').split('@')[1] const domain = account.get('acct').split('@')[1]
const isRemoteUser = !!domain 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 you want additional emoji handler, add statements below which set replacement and return true.
if (shortname in customEmojis) { if (shortname in customEmojis) {
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url; 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 true;
} }
return false; return false;
@ -60,7 +60,7 @@ const emojify = (str, customEmojis = {}) => {
} else { // matched to unicode emoji } else { // matched to unicode emoji
const { filename, shortCode } = unicodeMapping[match]; const { filename, shortCode } = unicodeMapping[match];
const title = shortCode ? `:${shortCode}:` : ''; 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; rend = i + match.length;
// If the matched character was followed by VS15 (for selecting text presentation), skip it. // If the matched character was followed by VS15 (for selecting text presentation), skip it.
if (str.codePointAt(rend) === 65038) { if (str.codePointAt(rend) === 65038) {

View File

@ -9,48 +9,39 @@ export default class ExtendedVideoPlayer extends PureComponent {
controls: PropTypes.bool.isRequired, controls: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired, muted: PropTypes.bool.isRequired,
onClick: PropTypes.func, onClick: PropTypes.func,
}; }
handleLoadedData = () => { handleLoadedData = () => {
if (this.props.time) { if (this.props.time) {
this.video.currentTime = this.props.time; this.video.currentTime = this.props.time
} }
} }
componentDidMount () { componentDidMount () {
this.video.addEventListener('loadeddata', this.handleLoadedData); this.video.addEventListener('loadeddata', this.handleLoadedData)
} }
componentWillUnmount () { componentWillUnmount () {
this.video.removeEventListener('loadeddata', this.handleLoadedData); this.video.removeEventListener('loadeddata', this.handleLoadedData)
} }
setRef = (c) => { setRef = (c) => {
this.video = c; this.video = c
} }
handleClick = e => { handleClick = e => {
e.stopPropagation(); e.stopPropagation()
const handler = this.props.onClick; const handler = this.props.onClick
if (handler) handler(); if (handler) handler()
} }
render () { render () {
const { src, muted, controls, alt } = this.props; 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);
// }
// }
return ( return (
<div className='extended-video-player'> <div className={[_s.default, _s.width100PC, _s.height100PC, _s.alignItemsCenter, _s.justifyContentCenter].join(' ')}>
<video <video
className={[_s.default, _s.maxWidth100PC, _s.heightMax100PC].join(' ')}
playsInline playsInline
ref={this.setRef} ref={this.setRef}
src={src} src={src}
@ -64,7 +55,7 @@ export default class ExtendedVideoPlayer extends PureComponent {
onClick={this.handleClick} onClick={this.handleClick}
/> />
</div> </div>
); )
} }
} }

View File

@ -21,7 +21,7 @@ const messages = defineMessages({
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
toggleMembership(group, relationships) { onToggleMembership(group, relationships) {
if (relationships.get('member')) { if (relationships.get('member')) {
dispatch(leaveGroup(group.get('id'))); dispatch(leaveGroup(group.get('id')));
} else { } else {
@ -116,6 +116,7 @@ class GroupHeader extends ImmutablePureComponent {
<Button <Button
radiusSmall radiusSmall
className={_s.mr5} className={_s.mr5}
onClick={this.handleOnToggleMembership}
{...actionButtonOptions} {...actionButtonOptions}
> >
<Text color='inherit' size='small'> <Text color='inherit' size='small'>

View File

@ -24,6 +24,7 @@ export default class Input extends PureComponent {
inputRef: PropTypes.func, inputRef: PropTypes.func,
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
hideLabel: PropTypes.bool, hideLabel: PropTypes.bool,
maxLength: PropTypes.number,
} }
handleOnChange = (e) => { handleOnChange = (e) => {
@ -46,7 +47,8 @@ export default class Input extends PureComponent {
readOnly, readOnly,
inputRef, inputRef,
id, id,
hideLabel hideLabel,
maxLength,
} = this.props } = this.props
const inputClasses = cx({ const inputClasses = cx({
@ -87,7 +89,7 @@ export default class Input extends PureComponent {
</Text> </Text>
</div> </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 && !!prependIcon &&
<Icon id={prependIcon} size='16px' className={[_s.fillPrimary, _s.ml15, _s.mr5].join(' ')} /> <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} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
readOnly={readOnly} readOnly={readOnly}
maxLength={maxLength}
/> />
{ {

View File

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

View File

@ -309,10 +309,11 @@ class MediaGallery extends PureComponent {
} = this.props } = this.props
const { visible } = this.state const { visible } = this.state
const width = this.state.width || defaultWidth; let width = this.state.width || defaultWidth
if (reduced) width = width / 2
const style = {}; const style = {}
const size = media.take(4).size; const size = media.take(4).size
const standard169 = width / (16 / 9); const standard169 = width / (16 / 9);
const standard169_percent = 100 / (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) //If reduced (i.e. like in a quoted post)
//then we need to make media smaller //then we need to make media smaller
if (reduced) { // if (reduced) {
style.height = width / 2 || '50%' // style.height = width / 2 || '50%'
} // }
if (!visible) { if (!visible) {
style.height = 'auto' style.height = 'auto'
@ -573,7 +574,7 @@ class MediaGallery extends PureComponent {
<Button <Button
title={intl.formatMessage(messages.toggle_visible)} title={intl.formatMessage(messages.toggle_visible)}
icon='hidden' icon='hidden'
backgroundColor='none' backgroundColor='black'
className={[_s.px10, _s.bgBlackOpaque_onHover].join(' ')} className={[_s.px10, _s.bgBlackOpaque_onHover].join(' ')}
onClick={this.handleOpen} onClick={this.handleOpen}
/> />

View File

@ -9,9 +9,9 @@ export default
class ConfirmationModal extends PureComponent { class ConfirmationModal extends PureComponent {
static propTypes = { static propTypes = {
title: PropTypes.node.isRequired, title: PropTypes.any.isRequired,
message: PropTypes.node.isRequired, message: PropTypes.any.isRequired,
confirm: PropTypes.string.isRequired, confirm: PropTypes.any.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired,
secondary: PropTypes.string, secondary: PropTypes.string,

View File

@ -49,8 +49,9 @@ export const mapDispatchToProps = (dispatch, { onClose }) => ({
} }
}, },
handleSelectResult: (resultId) => { handleSelectResult: (result) => {
dispatch(setSelectedGif(result))
onClose()
}, },
// dispatchSubmit: (e) => { // dispatchSubmit: (e) => {
@ -70,11 +71,11 @@ class GifPickerModal extends PureComponent {
handleCloseModal: PropTypes.func.isRequired, handleCloseModal: PropTypes.func.isRequired,
handleFetchCategories: PropTypes.func.isRequired, handleFetchCategories: PropTypes.func.isRequired,
handleOnChange: PropTypes.func.isRequired, handleOnChange: PropTypes.func.isRequired,
handleSelectResult: PropTypes.func.isRequired,
categories: PropTypes.array.isRequired, categories: PropTypes.array.isRequired,
results: PropTypes.array.isRequired, results: PropTypes.array.isRequired,
loading: PropTypes.bool, loading: PropTypes.bool,
error: PropTypes.bool, error: PropTypes.bool,
chosenUrl: PropTypes.string,
searchText: PropTypes.string, searchText: PropTypes.string,
} }
@ -98,8 +99,8 @@ class GifPickerModal extends PureComponent {
this.props.handleOnChange(category) this.props.handleOnChange(category)
} }
handleSelectGifResult = (resultId) => { handleSelectGifResult = (resultBlock) => {
console.log("handleSelectGifResult:", resultId) this.props.handleSelectResult(resultBlock)
} }
render() { render() {
@ -109,7 +110,7 @@ class GifPickerModal extends PureComponent {
results, results,
loading, loading,
error, error,
searchText searchText,
} = this.props } = this.props
return ( return (
@ -165,6 +166,7 @@ class GifPickerModal extends PureComponent {
} }
class GifResultsCollectionColumn extends PureComponent { class GifResultsCollectionColumn extends PureComponent {
static propTypes = { static propTypes = {
results: PropTypes.array.isRequired, results: PropTypes.array.isRequired,
handleSelectGifResult: PropTypes.func.isRequired, handleSelectGifResult: PropTypes.func.isRequired,
@ -183,8 +185,8 @@ class GifResultsCollectionColumn extends PureComponent {
results.map((result, i) => ( results.map((result, i) => (
<button <button
key={`gif-result-item-${i}`} key={`gif-result-item-${i}`}
onClick={() => this.onClick(result.id)} onClick={() => this.onClick(result)}
className={[_s.default, _s.cursorPointer, _s.px2, _s.py2].join(' ')} className={[_s.default, _s.outlineNone, _s.bgTransparent, _s.cursorPointer, _s.px2, _s.py2].join(' ')}
> >
<Image <Image
height={result.media[0].tinygif.dims[1]} height={result.media[0].tinygif.dims[1]}
@ -196,9 +198,11 @@ class GifResultsCollectionColumn extends PureComponent {
</div> </div>
) )
} }
} }
class GifResultsCollection extends PureComponent { class GifResultsCollection extends PureComponent {
static propTypes = { static propTypes = {
results: PropTypes.array.isRequired, results: PropTypes.array.isRequired,
handleSelectGifResult: PropTypes.func.isRequired, handleSelectGifResult: PropTypes.func.isRequired,
@ -250,7 +254,7 @@ class GifCategoriesCollection extends PureComponent {
<button <button
key={`gif-category-${i}`} key={`gif-category-${i}`}
onClick={() => this.onClick(category.searchterm)} 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(' ')}> <div className={[_s.default, _s.cursorPointer].join(' ')}>
<Image <Image
@ -269,4 +273,5 @@ class GifCategoriesCollection extends PureComponent {
</div> </div>
) )
} }
} }

View File

@ -158,19 +158,23 @@ class MediaModal extends ImmutablePureComponent {
pagination = media.map((item, i) => { pagination = media.map((item, i) => {
const btnClasses = CX({ const btnClasses = CX({
default: 1, default: 1,
px5: 1, width10PX: 1,
py5: 1, height10PX: 1,
outlineNone: 1, outlineNone: 1,
colorPrimary: 1,
circle: 1, circle: 1,
cursorPointer: 1, cursorPointer: 1,
colorPrimary: i === index,
lineHeight0825: i === index,
bgPrimaryOpaque: i !== index, bgPrimaryOpaque: i !== index,
bgPrimary: i === index, bgPrimary: i === index,
}) })
const activeText = i === index ? '•' : ''
return ( return (
<li className={[_s.default, _s.px5].join(' ')} key={`media-pagination-${i}`}> <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> </li>
) )
}) })
@ -234,6 +238,9 @@ class MediaModal extends ImmutablePureComponent {
const swipeableViewsStyle = { const swipeableViewsStyle = {
width: '100%', width: '100%',
height: '100%', height: '100%',
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
} }
const navigationClasses = CX({ const navigationClasses = CX({
@ -244,7 +251,7 @@ class MediaModal extends ImmutablePureComponent {
return ( return (
<div className={[_s.default, _s.width100PC, _s.height100PC, _s.alignItemsCenter, _s.justifyContentCenter].join(' ')}> <div className={[_s.default, _s.width100PC, _s.height100PC, _s.alignItemsCenter, _s.justifyContentCenter].join(' ')}>
<div <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' role='presentation'
onClick={onClose} onClick={onClose}
> >
@ -252,6 +259,7 @@ class MediaModal extends ImmutablePureComponent {
style={swipeableViewsStyle} style={swipeableViewsStyle}
containerStyle={{ containerStyle={{
alignItems: 'center', alignItems: 'center',
width: '100%',
}} }}
onChangeIndex={this.handleSwipe} onChangeIndex={this.handleSwipe}
onSwitching={this.handleSwitching} onSwitching={this.handleSwitching}
@ -286,7 +294,7 @@ class MediaModal extends ImmutablePureComponent {
</div> </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} {pagination}
</ul> </ul>
</div> </div>

View File

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

View File

@ -45,12 +45,7 @@ class ProUpgradeModal extends ImmutablePureComponent {
</div> </div>
<Button <Button
backgroundColor='brand'
color='white'
icon='pro'
href='https://pro.gab.com' href='https://pro.gab.com'
className={_s.justifyContentCenter}
iconClassName={[_s.mr5, _s.fillWhite].join(' ')}
> >
<Text color='inherit' weight='bold' align='center'> <Text color='inherit' weight='bold' align='center'>
{intl.formatMessage(messages.title)} {intl.formatMessage(messages.title)}

View File

@ -56,8 +56,8 @@ class ReportModal extends ImmutablePureComponent {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
} }
handleCommentChange = e => { handleCommentChange = (e) => {
this.props.dispatch(changeReportComment(e.target.value)) this.props.dispatch(changeReportComment(value))
} }
handleForwardChange = e => { handleForwardChange = e => {

View File

@ -75,9 +75,11 @@ class Poll extends ImmutablePureComponent {
const { selected } = this.state const { selected } = this.state
const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100 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 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 active = !!selected[`${optionIndex}`]
const showResults = poll.get('voted') || poll.get('expired') const showResults = poll.get('voted') || poll.get('expired')
const multiple = poll.get('multiple') const multiple = poll.get('multiple')
const correctedWidthPercent = optionHasNoVotes ? 100 : percent
let titleEmojified = option.get('title_emojified') let titleEmojified = option.get('title_emojified')
if (!titleEmojified) { if (!titleEmojified) {
@ -92,10 +94,12 @@ class Poll extends ImmutablePureComponent {
left0: 1, left0: 1,
radiusSmall: 1, radiusSmall: 1,
height100PC: 1, height100PC: 1,
bgSecondary: !leading, bgSecondary: !leading && !optionHasNoVotes,
bgTertiary: !leading && optionHasNoVotes,
bgBrandLight: leading, bgBrandLight: leading,
}) })
// : todo :
const inputClasses = cx('poll__input', { const inputClasses = cx('poll__input', {
'poll__input--checkbox': multiple, 'poll__input--checkbox': multiple,
'poll__input--active': active, 'poll__input--active': active,
@ -107,7 +111,7 @@ class Poll extends ImmutablePureComponent {
py10: showResults, py10: showResults,
mb10: 1, mb10: 1,
border1PX: !showResults, border1PX: !showResults,
fillSecondary: !showResults, borderColorSecondary: !showResults,
circle: !showResults, circle: !showResults,
cursorPointer: !showResults, cursorPointer: !showResults,
bgSubtle_onHover: !showResults, bgSubtle_onHover: !showResults,
@ -127,7 +131,7 @@ class Poll extends ImmutablePureComponent {
<li className={listItemClasses} key={option.get('title')}> <li className={listItemClasses} key={option.get('title')}>
{ {
showResults && ( 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 }) => {({ width }) =>
<span className={chartClasses} style={{ width: `${width}%` }} /> <span className={chartClasses} style={{ width: `${width}%` }} />
} }
@ -139,7 +143,7 @@ class Poll extends ImmutablePureComponent {
<Text <Text
size='medium' size='medium'
color='primary' color='primary'
weight={leading ? 'bold' : 'normal'} weight={(leading && showResults) ? 'bold' : 'normal'}
className={[_s.displayFlex, _s.flexRow, _s.width100PC, _s.alignItemsCenter].join(' ')} className={[_s.displayFlex, _s.flexRow, _s.width100PC, _s.alignItemsCenter].join(' ')}
> >
{ {

View File

@ -1,8 +1,17 @@
import DatePicker from 'react-datepicker' import DatePicker from 'react-datepicker'
import { FormattedMessage } from 'react-intl'
import moment from 'moment-mini'
import { changeScheduledAt } from '../../actions/compose' import { changeScheduledAt } from '../../actions/compose'
import { openModal } from '../../actions/modal'
import { closePopover } from '../../actions/popover'
import { me } from '../../initial_state' import { me } from '../../initial_state'
import {
MODAL_PRO_UPGRADE,
} from '../../constants'
import { isMobile } from '../../utils/is_mobile' import { isMobile } from '../../utils/is_mobile'
import PopoverLayout from './popover_layout' import PopoverLayout from './popover_layout'
import Button from '../button'
import Text from '../text'
import '!style-loader!css-loader!react-datepicker/dist/react-datepicker.css' 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']), isPro: state.getIn(['accounts', me, 'is_pro']),
}) })
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch, { isPro }) => ({
setScheduledAt (date) { setScheduledAt (date) {
if (!isPro) {
dispatch(closePopover())
return dispatch(openModal(MODAL_PRO_UPGRADE))
}
dispatch(changeScheduledAt(date)) dispatch(changeScheduledAt(date))
if (!date) {
dispatch(closePopover())
}
}, },
}) })
@ -32,15 +50,19 @@ class DatePickerPopover extends PureComponent {
this.props.setScheduledAt(date) this.props.setScheduledAt(date)
} }
render() { handleRemoveDate = () => {
const { date, isPro, position } = this.props this.props.setScheduledAt(null)
}
render() {
const { date, isPro } = this.props
const open = !!date
const datePickerDisabled = !isPro const datePickerDisabled = !isPro
const withPortal = isMobile(window.innerWidth) const withPortal = isMobile(window.innerWidth)
return ( return (
<PopoverLayout width={331}> <PopoverLayout width={331}>
<div className={[_s.default].join(' ')}>
<DatePicker <DatePicker
inline inline
target={this} target={this}
@ -67,6 +89,30 @@ class DatePickerPopover extends PureComponent {
} }
}} }}
/> />
</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> </PopoverLayout>
) )
} }

View File

@ -7,6 +7,7 @@ import detectPassiveEvents from 'detect-passive-events'
import { changeSetting } from '../../actions/settings' import { changeSetting } from '../../actions/settings'
import { useEmoji } from '../../actions/emojis' import { useEmoji } from '../../actions/emojis'
import { closePopover } from '../../actions/popover' import { closePopover } from '../../actions/popover'
import { insertEmojiCompose } from '../../actions/compose'
import { EmojiPicker as EmojiPickerAsync } from '../../features/ui/util/async_components' import { EmojiPicker as EmojiPickerAsync } from '../../features/ui/util/async_components'
import { buildCustomEmojis } from '../emoji/emoji' import { buildCustomEmojis } from '../emoji/emoji'
import PopoverLayout from './popover_layout' import PopoverLayout from './popover_layout'
@ -209,21 +210,19 @@ const mapStateToProps = (state) => ({
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state), frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
}) })
const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({ const mapDispatchToProps = (dispatch) => ({
onClosePopover() { onClosePopover() {
dispatch(closePopover()) dispatch(closePopover())
}, },
onSkinTone: skinTone => { onSkinTone: (skinTone) => {
dispatch(changeSetting(['skinTone'], skinTone)) dispatch(changeSetting(['skinTone'], skinTone))
}, },
onPickEmoji: emoji => { onPickEmoji: (emoji) => {
dispatch(useEmoji(emoji)) dispatch(useEmoji(emoji))
console.log("emoji:", emoji)
if (onPickEmoji) { dispatch(insertEmojiCompose(0, emoji, false))
onPickEmoji(emoji)
}
}, },
}) })

View File

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

View File

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

View File

@ -67,6 +67,10 @@ export default
@injectIntl @injectIntl
class Sidebar extends ImmutablePureComponent { class Sidebar extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
}
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
account: ImmutablePropTypes.map, 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 => { setMoreButtonRef = n => {
this.moreBtnRef = n this.moreBtnRef = n
} }
@ -213,7 +229,17 @@ class Sidebar extends ImmutablePureComponent {
<div className={_s.default}> <div className={_s.default}>
{ {
!!title && !!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'> <Heading size='h1'>
{title} {title}
</Heading> </Heading>

View File

@ -10,6 +10,7 @@ import ComposeFormContainer from '../features/compose/containers/compose_form_co
import StatusContent from './status_content' import StatusContent from './status_content'
import StatusPrepend from './status_prepend' import StatusPrepend from './status_prepend'
import StatusActionBar from './status_action_bar' import StatusActionBar from './status_action_bar'
import StatusMedia from './status_media'
import Poll from './poll' import Poll from './poll'
import StatusHeader from './status_header' import StatusHeader from './status_header'
import CommentList from './comment_list' import CommentList from './comment_list'
@ -87,6 +88,7 @@ class Status extends ImmutablePureComponent {
cacheMediaWidth: PropTypes.func, cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number, cachedMediaWidth: PropTypes.number,
contextType: PropTypes.string, contextType: PropTypes.string,
commentsLimited: PropTypes.bool,
} }
// Avoid checking props that are functions (and whose equality will always // Avoid checking props that are functions (and whose equality will always
@ -124,7 +126,7 @@ class Status extends ImmutablePureComponent {
} }
static getDerivedStateFromProps(nextProps, prevState) { static getDerivedStateFromProps(nextProps, prevState) {
if (!nextProps.isHidden && nextProps.isIntersecting && !prevState.loadedComments) { if (!nextProps.isHidden && (nextProps.isIntersecting || !nextProps.commentsLimited) && !prevState.loadedComments) {
return { return {
loadedComments: true loadedComments: true
} }
@ -132,6 +134,7 @@ class Status extends ImmutablePureComponent {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) { if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return { return {
loadedComments: false,
showMedia: defaultMediaVisibility(nextProps.status), showMedia: defaultMediaVisibility(nextProps.status),
statusId: nextProps.status.get('id'), statusId: nextProps.status.get('id'),
} }
@ -142,6 +145,7 @@ class Status extends ImmutablePureComponent {
// Compensate height changes // Compensate height changes
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
// timeline lazy loading comments
if (!prevState.loadedComments && this.state.loadedComments && this.props.status) { if (!prevState.loadedComments && this.state.loadedComments && this.props.status) {
const commentCount = this.props.status.get('replies_count') const commentCount = this.props.status.get('replies_count')
if (commentCount > 0) { if (commentCount > 0) {
@ -163,7 +167,7 @@ class Status extends ImmutablePureComponent {
} }
} }
handleMoveUp = id => { handleMoveUp = (id) => {
const { status, ancestorsIds, descendantsIds } = this.props const { status, ancestorsIds, descendantsIds } = this.props
if (id === status.get('id')) { if (id === status.get('id')) {
@ -347,7 +351,7 @@ class Status extends ImmutablePureComponent {
if (!status) return null if (!status) return null
let media, reblogContent, rebloggedByText = null let reblogContent, rebloggedByText = null
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
rebloggedByText = intl.formatMessage( rebloggedByText = intl.formatMessage(
@ -386,62 +390,6 @@ class Status extends ImmutablePureComponent {
return null 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({ const containerClasses = cx({
default: 1, default: 1,
pb15: isFeatured, pb15: isFeatured,
@ -476,7 +424,7 @@ class Status extends ImmutablePureComponent {
data-featured={isFeatured ? 'true' : null} data-featured={isFeatured ? 'true' : null}
aria-label={textForScreenReader(intl, status, rebloggedByText)} aria-label={textForScreenReader(intl, status, rebloggedByText)}
ref={this.handleRef} ref={this.handleRef}
// onClick={this.handleClick} onClick={isChild ? this.handleClick : undefined}
> >
<div className={innerContainerClasses}> <div className={innerContainerClasses}>
@ -497,7 +445,17 @@ class Status extends ImmutablePureComponent {
/> />
</div> </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 && !!status.get('quote') && !isChild &&

View File

@ -42,7 +42,7 @@ class StatusActionBar extends ImmutablePureComponent {
updateOnProps = ['status'] updateOnProps = ['status']
handleReplyClick = () => { handleReplyClick = () => {
this.props.onReply(this.props.status) this.props.onReply(this.props.status, null, true)
} }
handleFavoriteClick = () => { handleFavoriteClick = () => {

View File

@ -10,8 +10,8 @@ import Text from './text'
const MAX_HEIGHT = 200 const MAX_HEIGHT = 200
const messages = defineMessages({ const messages = defineMessages({
showMore: { id: 'status.show_more', defaultMessage: 'Show more' }, show: { id: 'status.show_more', defaultMessage: 'Show' },
showLess: { id: 'status.show_less', defaultMessage: 'Show less' }, hide: { id: 'status.show_less', defaultMessage: 'Hide' },
readMore: { id: 'status.read_more', defaultMessage: 'Read more' }, 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({ const spoilerContainerClasses = cx({
default: 1, default: 1,

View File

@ -1,4 +1,5 @@
import { Fragment } from 'react' import { Fragment } from 'react'
import { injectIntl, defineMessages } from 'react-intl'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import ImmutablePropTypes from 'react-immutable-proptypes' import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePureComponent from 'react-immutable-pure-component'
@ -14,6 +15,14 @@ import Icon from './icon'
import Button from './button' import Button from './button'
import Avatar from './avatar' 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 cx = classNames.bind(_s)
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
@ -33,10 +42,12 @@ const mapDispatchToProps = (dispatch) => ({
}) })
export default export default
@injectIntl
@connect(null, mapDispatchToProps) @connect(null, mapDispatchToProps)
class StatusHeader extends ImmutablePureComponent { class StatusHeader extends ImmutablePureComponent {
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired,
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
onOpenStatusRevisionsPopover: PropTypes.func.isRequired, onOpenStatusRevisionsPopover: PropTypes.func.isRequired,
onOpenStatusOptionsPopover: PropTypes.func.isRequired, onOpenStatusOptionsPopover: PropTypes.func.isRequired,
@ -56,7 +67,11 @@ class StatusHeader extends ImmutablePureComponent {
} }
render() { render() {
const { status, reduced } = this.props const {
intl,
reduced,
status,
} = this.props
const statusUrl = `/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}` const statusUrl = `/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`
@ -68,7 +83,22 @@ class StatusHeader extends ImmutablePureComponent {
}) })
const avatarSize = reduced ? 20 : 46 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 ( return (
<div className={containerClasses}> <div className={containerClasses}>
@ -127,7 +157,9 @@ class StatusHeader extends ImmutablePureComponent {
<DotTextSeperator /> <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') && !!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 MIN_SCALE = 1
const MAX_SCALE = 4; const MAX_SCALE = 4
const getMidpoint = (p1, p2) => ({ const getMidpoint = (p1, p2) => ({
x: (p1.clientX + p2.clientX) / 2, x: (p1.clientX + p2.clientX) / 2,
y: (p1.clientY + p2.clientY) / 2, y: (p1.clientY + p2.clientY) / 2,
}); })
const getDistance = (p1, p2) => const getDistance = (p1, p2) => {
Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2)); 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)); const clamp = (min, max, value) => {
return 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 :
export default class ZoomableImage extends PureComponent { export default class ZoomableImage extends PureComponent {
@ -40,69 +28,69 @@ export default class ZoomableImage extends PureComponent {
alt: '', alt: '',
width: null, width: null,
height: null, height: null,
}; }
state = { state = {
scale: MIN_SCALE, scale: MIN_SCALE,
} }
removers = []; removers = []
container = null; container = null
image = null; image = null
lastTouchEndTime = 0; lastTouchEndTime = 0
lastDistance = 0; lastDistance = 0
componentDidMount () { componentDidMount () {
let handler = this.handleTouchStart; let handler = this.handleTouchStart
this.container.addEventListener('touchstart', handler); this.container.addEventListener('touchstart', handler)
this.removers.push(() => this.container.removeEventListener('touchstart', handler)); this.removers.push(() => this.container.removeEventListener('touchstart', handler))
handler = this.handleTouchMove; handler = this.handleTouchMove
// on Chrome 56+, touch event listeners will default to passive // on Chrome 56+, touch event listeners will default to passive
// https://www.chromestatus.com/features/5093566007214080 // https://www.chromestatus.com/features/5093566007214080
this.container.addEventListener('touchmove', handler, { passive: false }); this.container.addEventListener('touchmove', handler, { passive: false })
this.removers.push(() => this.container.removeEventListener('touchend', handler)); this.removers.push(() => this.container.removeEventListener('touchend', handler))
} }
componentWillUnmount () { componentWillUnmount () {
this.removeEventListeners(); this.removeEventListeners()
} }
removeEventListeners () { removeEventListeners () {
this.removers.forEach(listeners => listeners()); this.removers.forEach(listeners => listeners())
this.removers = []; this.removers = []
} }
handleTouchStart = e => { 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 => { handleTouchMove = e => {
const { scrollTop, scrollHeight, clientHeight } = this.container; const { scrollTop, scrollHeight, clientHeight } = this.container
if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) { if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) {
// prevent propagating event to MediaModal // prevent propagating event to MediaModal
e.stopPropagation(); e.stopPropagation()
return; return
} }
if (e.touches.length !== 2) return; if (e.touches.length !== 2) return
e.preventDefault(); e.preventDefault()
e.stopPropagation(); e.stopPropagation()
const distance = getDistance(...e.touches); const distance = getDistance(...e.touches)
const midpoint = getMidpoint(...e.touches); const midpoint = getMidpoint(...e.touches)
const scale = clamp(MIN_SCALE, MAX_SCALE, this.state.scale * distance / this.lastDistance); 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.lastMidpoint = midpoint
this.lastDistance = distance; this.lastDistance = distance
} }
zoom(nextScale, midpoint) { zoom(nextScale, midpoint) {
const { scale } = this.state; const { scale } = this.state
const { scrollLeft, scrollTop } = this.container; const { scrollLeft, scrollTop } = this.container
// math memo: // math memo:
// x = (scrollLeft + midpoint.x) / scrollWidth // x = (scrollLeft + midpoint.x) / scrollWidth
@ -110,38 +98,44 @@ export default class ZoomableImage extends PureComponent {
// scrollWidth = clientWidth * scale // scrollWidth = clientWidth * scale
// scrollWidth' = clientWidth * nextScale // scrollWidth' = clientWidth * nextScale
// Solve x = x' for nextScrollLeft // Solve x = x' for nextScrollLeft
const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x; const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x
const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y; const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y
this.setState({ scale: nextScale }, () => { this.setState({ scale: nextScale }, () => {
this.container.scrollLeft = nextScrollLeft; this.container.scrollLeft = nextScrollLeft
this.container.scrollTop = nextScrollTop; this.container.scrollTop = nextScrollTop
}); })
} }
handleClick = e => { handleClick = e => {
// don't propagate event to MediaModal // don't propagate event to MediaModal
e.stopPropagation(); e.stopPropagation()
const handler = this.props.onClick; const handler = this.props.onClick
if (handler) handler(); if (handler) handler()
} }
setContainerRef = c => { setContainerRef = c => {
this.container = c; this.container = c
} }
setImageRef = c => { setImageRef = c => {
this.image = c; this.image = c
} }
render () { render () {
const { alt, src } = this.props; const { alt, src } = this.props
const { scale } = this.state; const { scale } = this.state
const overflow = scale === 1 ? 'hidden' : 'scroll';
const overflow = scale === 1 ? 'hidden' : 'scroll'
return ( 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 <img
className={[_s.default, _s.objectFitContain, _s.heightAuto, _s.widthAuto, _s.maxWidth100PC, _s.heightMax100PC].join(' ')}
role='presentation' role='presentation'
ref={this.setImageRef} ref={this.setImageRef}
alt={alt} alt={alt}
@ -154,7 +148,7 @@ export default class ZoomableImage extends PureComponent {
onClick={this.handleClick} onClick={this.handleClick}
/> />
</div> </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 { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { import {
replyCompose, replyCompose,
@ -34,16 +34,8 @@ import {
import { makeGetStatus } from '../selectors'; import { makeGetStatus } from '../selectors';
import Status from '../components/status'; 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 makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus()
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const statusId = props.id || props.params.statusId const statusId = props.id || props.params.statusId
@ -63,8 +55,11 @@ const makeMapStateToProps = () => {
descendantsIds = descendantsIds.withMutations(mutable => { descendantsIds = descendantsIds.withMutations(mutable => {
const ids = [status.get('id')] const ids = [status.get('id')]
const r = state.getIn(['contexts', 'replies', ids[0]])
console.log("r:", r)
while (ids.length > 0) { while (ids.length > 0) {
let id = ids.shift(); let id = ids.shift()
const replies = state.getIn(['contexts', 'replies', id]) const replies = state.getIn(['contexts', 'replies', id])
if (status.get('id') !== id) { if (status.get('id') !== id) {
@ -94,22 +89,22 @@ const makeMapStateToProps = () => {
return mapStateToProps return mapStateToProps
}; };
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch) => ({
onReply (status, router) { onReply (status, router, showModal) {
if (!me) return dispatch(openModal('UNAUTHORIZED')) if (!me) return dispatch(openModal('UNAUTHORIZED'))
dispatch((_, getState) => { dispatch((_, getState) => {
const state = getState(); const state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) { if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.replyMessage), 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: intl.formatMessage(messages.replyConfirm), confirm: <FormattedMessage id='confirmations.reply.confirm' defaultMessage='Reply' />,
onConfirm: () => dispatch(replyCompose(status, router)), onConfirm: () => dispatch(replyCompose(status, router)),
})); }))
} else { } else {
dispatch(replyCompose(status, router)); dispatch(replyCompose(status, router, showModal));
} }
}); })
}, },
onRepost (targetRef, status, e) { onRepost (targetRef, status, e) {
@ -177,8 +172,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(deleteStatus(status.get('id'), history)); dispatch(deleteStatus(status.get('id'), history));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.deleteMessage), message: <FormattedMessage id='confirmations.delete.message' defaultMessage='Are you sure you want to delete this status?' />,
confirm: intl.formatMessage(messages.deleteConfirm), confirm: <FormattedMessage id='confirmations.delete.confirm' defaultMessage='Delete' />,
onConfirm: () => dispatch(deleteStatus(status.get('id'), history)), 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 StatusVisibilityButton from './status_visibility_button'
import UploadButton from './media_upload_button' import UploadButton from './media_upload_button'
import UploadForm from './upload_form' import UploadForm from './upload_form'
import GifForm from './gif_form'
import Input from '../../../components/input'
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: "What's on your mind?" }, placeholder: { id: 'compose_form.placeholder', defaultMessage: "What's on your mind?" },
@ -78,6 +80,7 @@ class ComposeForm extends ImmutablePureComponent {
replyToId: PropTypes.string, replyToId: PropTypes.string,
reduxReplyToId: PropTypes.string, reduxReplyToId: PropTypes.string,
hasPoll: PropTypes.bool, hasPoll: PropTypes.bool,
selectedGifSrc: PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
@ -85,7 +88,7 @@ class ComposeForm extends ImmutablePureComponent {
}; };
handleChange = (e, markdown) => { handleChange = (e, markdown) => {
this.props.onChange(e.target.value, markdown); this.props.onChange(e.target.value, markdown, this.props.replyToId)
} }
handleComposeFocus = () => { handleComposeFocus = () => {
@ -156,8 +159,8 @@ class ComposeForm extends ImmutablePureComponent {
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']); this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
} }
handleChangeSpoilerText = (e) => { handleChangeSpoilerText = (value) => {
this.props.onChangeSpoilerText(e.target.value); this.props.onChangeSpoilerText(value)
} }
componentDidMount() { componentDidMount() {
@ -169,7 +172,7 @@ class ComposeForm extends ImmutablePureComponent {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (!this.autosuggestTextarea) return; if (!this.autosuggestTextarea) return
// This statement does several things: // This statement does several things:
// - If we're beginning a reply, and, // - If we're beginning a reply, and,
@ -196,17 +199,13 @@ class ComposeForm extends ImmutablePureComponent {
} }
setAutosuggestTextarea = (c) => { setAutosuggestTextarea = (c) => {
this.autosuggestTextarea = c; this.autosuggestTextarea = c
} }
setForm = (c) => { setForm = (c) => {
this.form = c this.form = c
} }
setSpoilerText = (c) => {
this.spoilerText = c
}
handleEmojiPick = (data) => { handleEmojiPick = (data) => {
const { text } = this.props const { text } = this.props
const position = this.autosuggestTextarea.textbox.selectionStart const position = this.autosuggestTextarea.textbox.selectionStart
@ -236,6 +235,7 @@ class ComposeForm extends ImmutablePureComponent {
isMatch, isMatch,
isChangingUpload, isChangingUpload,
isSubmitting, isSubmitting,
selectedGifSrc,
} = this.props } = this.props
const disabled = isSubmitting const disabled = isSubmitting
const text = [this.props.spoilerText, countableText(this.props.text)].join(''); const text = [this.props.spoilerText, countableText(this.props.text)].join('');
@ -294,7 +294,7 @@ class ComposeForm extends ImmutablePureComponent {
> >
{ {
!!reduxReplyToId && !shouldCondense && !!reduxReplyToId && !shouldCondense && isModalOpen &&
<div className={[_s.default, _s.px15, _s.py10, _s.mt5].join(' ')}> <div className={[_s.default, _s.px15, _s.py10, _s.mt5].join(' ')}>
<StatusContainer <StatusContainer
id={reduxReplyToId} id={reduxReplyToId}
@ -306,19 +306,13 @@ class ComposeForm extends ImmutablePureComponent {
{ {
!!spoiler && !!spoiler &&
<div className={[_s.default, _s.px15, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}> <div className={[_s.default, _s.px15, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
<AutosuggestTextbox <Input
placeholder={intl.formatMessage(messages.spoiler_placeholder)} placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText} value={this.props.spoilerText}
onChange={this.handleChangeSpoilerText} onChange={this.handleChangeSpoilerText}
onKeyDown={this.handleKeyDown}
disabled={!this.props.spoiler} disabled={!this.props.spoiler}
ref={this.setSpoilerText}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSpoilerSuggestionSelected}
searchTokens={[':']}
prependIcon='warning' prependIcon='warning'
maxLength={256}
id='cw-spoiler-input' id='cw-spoiler-input'
/> />
</div> </div>
@ -349,10 +343,15 @@ class ComposeForm extends ImmutablePureComponent {
</div> </div>
} }
{ /* : todo : for gif {
(isUploading || hasGif) && /*
!!selectedGifSrc && !anyMedia &&
<div className={[_s.default, _s.px15].join(' ')}> <div className={[_s.default, _s.px15].join(' ')}>
<UploadForm replyToId={replyToId} /> <GifForm
replyToId={replyToId}
small={shouldCondense}
selectedGifSrc={selectedGifSrc}
/>
</div> </div>
*/ */
} }
@ -365,7 +364,7 @@ class ComposeForm extends ImmutablePureComponent {
} }
{ {
!!quoteOfId && !!quoteOfId && isModalOpen &&
<div className={[_s.default, _s.px15, _s.py10, _s.mt5].join(' ')}> <div className={[_s.default, _s.px15, _s.py10, _s.mt5].join(' ')}>
<StatusContainer <StatusContainer
id={quoteOfId} id={quoteOfId}
@ -376,29 +375,21 @@ class ComposeForm extends ImmutablePureComponent {
<div className={actionsContainerClasses}> <div className={actionsContainerClasses}>
<div className={[_s.default, _s.flexRow, _s.mrAuto].join(' ')}> <div className={[_s.default, _s.flexRow, _s.mrAuto].join(' ')}>
{
!shouldCondense && <EmojiPickerButton small={shouldCondense} isMatch={isMatch} />
<RichTextEditorButton />
}
<UploadButton small={shouldCondense} /> <UploadButton small={shouldCondense} />
{ /* <GifSelectorButton small={shouldCondense} /> */ }
{ {
!edit && !shouldCondense && !edit && !shouldCondense &&
<PollButton /> <PollButton />
} }
{
!shouldCondense && { !shouldCondense && <StatusVisibilityButton /> }
<StatusVisibilityButton /> { !shouldCondense && <SpoilerButton /> }
} { !shouldCondense && <SchedulePostButton /> }
{ { /* !shouldCondense && <RichTextEditorButton /> */ }
!shouldCondense &&
<SpoilerButton />
}
{
!shouldCondense &&
<SchedulePostButton />
}
<GifSelectorButton small={shouldCondense} />
<EmojiPickerButton small={shouldCondense} isMatch={isMatch} />
{ {
shouldCondense && shouldCondense &&

View File

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

View File

@ -12,7 +12,7 @@ const messages = defineMessages({
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), 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, unavailable: state.getIn(['compose', 'poll']) !== null,
resetFileKey: state.getIn(['compose', 'resetFileKey']), resetFileKey: state.getIn(['compose', 'resetFileKey']),
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -345,7 +345,7 @@
"status.report": "Report @{name}", "status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content", "status.sensitive_warning": "Sensitive content",
"status.share": "Share", "status.share": "Share",
"status.show_less": "Show less", "status.show_less": "Hide",
"status.show_less_all": "Show less for all", "status.show_less_all": "Show less for all",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.show_more_all": "Show more for all", "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 insertEmoji = (state, position, emojiData, needsSpace) => {
const oldText = state.get('text'); const oldText = state.get('text')
const emoji = needsSpace ? ' ' + emojiData.native : emojiData.native; 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({ return state.merge({
text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`, text,
focusDate: new Date(), focusDate: new Date(),
caretPosition: position + emoji.length + 1, caretPosition: position + emoji.length + 1,
idempotencyKey: uuid(), idempotencyKey: uuid(),
@ -248,10 +250,14 @@ export default function compose(state = initialState, action) {
.set('privacy', action.value) .set('privacy', action.value)
.set('idempotencyKey', uuid()); .set('idempotencyKey', uuid());
case COMPOSE_CHANGE: case COMPOSE_CHANGE:
return state return state.withMutations(map => {
.set('text', action.text) map.set('text', action.text)
.set('markdown', action.markdown) map.set('markdown', action.markdown)
.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid())
if (action.replyId) {
map.set('in_reply_to', action.replyId)
}
})
case COMPOSE_COMPOSING_CHANGE: case COMPOSE_COMPOSING_CHANGE:
return state.set('is_composing', action.value); return state.set('is_composing', action.value);
case COMPOSE_REPLY: case COMPOSE_REPLY:

View File

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

View File

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

View File

@ -3,7 +3,7 @@
class HashtagQueryService < BaseService class HashtagQueryService < BaseService
LIMIT_PER_MODE = 1 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) tags = tags_for(Array(tag.name) | Array(params[:any])).pluck(:id)
all = tags_for(params[:all]) all = tags_for(params[:all])
none = tags_for(params[:none]) 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)) @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 end
def language_from_option(str) def language_from_option(str)

View File

@ -724,7 +724,7 @@ en:
limit: You have reached the maximum amount of lists limit: You have reached the maximum amount of lists
media_attachments: media_attachments:
validations: 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 too_many: Cannot attach more than 4 files
migrations: migrations:
acct: username@domain of the new account acct: username@domain of the new account

View File

@ -685,7 +685,7 @@ en_GB:
limit: You have reached the maximum amount of lists limit: You have reached the maximum amount of lists
media_attachments: media_attachments:
validations: 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 too_many: Cannot attach more than 4 files
migrations: migrations:
acct: username@domain of the new account acct: username@domain of the new account

View File

@ -181,7 +181,7 @@ io:
upload: Kargar upload: Kargar
media_attachments: media_attachments:
validations: 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 too_many: Cannot attach more than 4 files
notification_mailer: notification_mailer:
digest: digest:

View File

@ -118,6 +118,7 @@
"markdown-draft-js": "^2.2.0", "markdown-draft-js": "^2.2.0",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"moment-mini": "^2.24.0",
"npmlog": "^4.1.2", "npmlog": "^4.1.2",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"object-fit-images": "^3.2.3", "object-fit-images": "^3.2.3",
@ -145,6 +146,7 @@
"react-router-scroll-4": "^1.0.0-beta.1", "react-router-scroll-4": "^1.0.0-beta.1",
"react-stickynode": "^2.1.1", "react-stickynode": "^2.1.1",
"react-swipeable-views": "^0.13.0", "react-swipeable-views": "^0.13.0",
"react-textarea-autosize": "^7.1.2",
"redis": "^2.7.1", "redis": "^2.7.1",
"redux": "^4.0.1", "redux": "^4.0.1",
"redux-immutable": "^4.0.0", "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: dependencies:
minimist "^1.2.5" 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: moment-timezone@^0.5.x:
version "0.5.28" version "0.5.28"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.28.tgz#f093d789d091ed7b055d82aa81a82467f72e4338" 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" react-is "^16.8.6"
scheduler "^0.19.1" 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: react@^16.13.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"