diff options
author | Starfall <root@starfall.blue> | 2020-02-04 17:44:29 -0600 |
---|---|---|
committer | Starfall <root@starfall.blue> | 2020-02-04 17:44:29 -0600 |
commit | 6d24d3bcb84abd04f31da95f97f6d60ef0afdc00 (patch) | |
tree | e7c38251a9e92bdf3a464b4aa7f1880aa5139bf0 /app/javascript/flavours/glitch/components | |
parent | c0c9529df269816f52915a9802e5e30fbce9576b (diff) | |
parent | 885e9227c6e8e1ce5e4a5625d5126ba76dce2c00 (diff) |
Merge branch 'glitch'
Diffstat (limited to 'app/javascript/flavours/glitch/components')
6 files changed, 116 insertions, 69 deletions
diff --git a/app/javascript/flavours/glitch/components/animated_number.js b/app/javascript/flavours/glitch/components/animated_number.js new file mode 100644 index 000000000..e3235e368 --- /dev/null +++ b/app/javascript/flavours/glitch/components/animated_number.js @@ -0,0 +1,65 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedNumber } from 'react-intl'; +import TransitionMotion from 'react-motion/lib/TransitionMotion'; +import spring from 'react-motion/lib/spring'; +import { reduceMotion } from 'flavours/glitch/util/initial_state'; + +export default class AnimatedNumber extends React.PureComponent { + + static propTypes = { + value: PropTypes.number.isRequired, + }; + + state = { + direction: 1, + }; + + componentWillReceiveProps (nextProps) { + if (nextProps.value > this.props.value) { + this.setState({ direction: 1 }); + } else if (nextProps.value < this.props.value) { + this.setState({ direction: -1 }); + } + } + + willEnter = () => { + const { direction } = this.state; + + return { y: -1 * direction }; + } + + willLeave = () => { + const { direction } = this.state; + + return { y: spring(1 * direction, { damping: 35, stiffness: 400 }) }; + } + + render () { + const { value } = this.props; + const { direction } = this.state; + + if (reduceMotion) { + return <FormattedNumber value={value} />; + } + + const styles = [{ + key: `${value}`, + data: value, + style: { y: spring(0, { damping: 35, stiffness: 400 }) }, + }]; + + return ( + <TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}> + {items => ( + <span className='animated-number'> + {items.map(({ key, data, style }) => ( + <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}><FormattedNumber value={data} /></span> + ))} + </span> + )} + </TransitionMotion> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js index dd1162429..01bd4a246 100644 --- a/app/javascript/flavours/glitch/components/column_header.js +++ b/app/javascript/flavours/glitch/components/column_header.js @@ -2,18 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { createPortal } from 'react-dom'; import classNames from 'classnames'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import Icon from 'flavours/glitch/components/icon'; -import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container'; - const messages = defineMessages({ show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' }, moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, - enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, }); export default @injectIntl @@ -28,26 +24,21 @@ class ColumnHeader extends React.PureComponent { title: PropTypes.node, icon: PropTypes.string, active: PropTypes.bool, - localSettings : ImmutablePropTypes.map, multiColumn: PropTypes.bool, extraButton: PropTypes.node, showBackButton: PropTypes.bool, - notifCleaning: PropTypes.bool, // true only for the notification column - notifCleaningActive: PropTypes.bool, - onEnterCleaningMode: PropTypes.func, children: PropTypes.node, pinned: PropTypes.bool, placeholder: PropTypes.bool, onPin: PropTypes.func, onMove: PropTypes.func, onClick: PropTypes.func, - intl: PropTypes.object.isRequired, + appendContent: PropTypes.node, }; state = { collapsed: true, animating: false, - animatingNCD: false, }; historyBack = (skip) => { @@ -89,10 +80,6 @@ class ColumnHeader extends React.PureComponent { this.setState({ animating: false }); } - handleTransitionEndNCD = () => { - this.setState({ animatingNCD: false }); - } - handlePin = () => { if (!this.props.pinned) { this.historyBack(); @@ -100,16 +87,9 @@ class ColumnHeader extends React.PureComponent { this.props.onPin(); } - onEnterCleaningMode = () => { - this.setState({ animatingNCD: true }); - this.props.onEnterCleaningMode(!this.props.notifCleaningActive); - } - render () { - const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive, placeholder } = this.props; - const { collapsed, animating, animatingNCD } = this.state; - - let title = this.props.title; + const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent } = this.props; + const { collapsed, animating } = this.state; const wrapperClassName = classNames('column-header__wrapper', { 'active': active, @@ -128,20 +108,8 @@ class ColumnHeader extends React.PureComponent { 'active': !collapsed, }); - const notifCleaningButtonClassName = classNames('column-header__button', { - 'active': notifCleaningActive, - }); - - const notifCleaningDrawerClassName = classNames('ncd column-header__collapsible', { - 'collapsed': !notifCleaningActive, - 'animating': animatingNCD, - }); - let extraContent, pinButton, moveButtons, backButton, collapseButton; - //*glitch - const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning); - if (children) { extraContent = ( <div key='extra-content' className='column-header__collapsible__extra'> @@ -202,33 +170,17 @@ class ColumnHeader extends React.PureComponent { <div className='column-header__buttons'> {hasTitle && backButton} {extraButton} - { notifCleaning ? ( - <button - aria-label={msgEnterNotifCleaning} - title={msgEnterNotifCleaning} - onClick={this.onEnterCleaningMode} - className={notifCleaningButtonClassName} - > - <Icon id='eraser' /> - </button> - ) : null} {collapseButton} </div> </h1> - { notifCleaning ? ( - <div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}> - <div className='column-header__collapsible-inner nopad-drawer'> - {(notifCleaningActive || animatingNCD) ? (<NotificationPurgeButtonsContainer />) : null } - </div> - </div> - ) : null} - <div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}> <div className='column-header__collapsible-inner'> {(!collapsed || animating) && collapsedContent} </div> </div> + + {appendContent} </div> ); diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index ab5b7a572..cc4d714e8 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -184,7 +184,7 @@ export default class Dropdown extends React.PureComponent { icon: PropTypes.string.isRequired, items: PropTypes.array.isRequired, size: PropTypes.number.isRequired, - ariaLabel: PropTypes.string, + title: PropTypes.string, disabled: PropTypes.bool, status: ImmutablePropTypes.map, isUserTouching: PropTypes.func, @@ -197,7 +197,7 @@ export default class Dropdown extends React.PureComponent { }; static defaultProps = { - ariaLabel: 'Menu', + title: 'Menu', }; state = { @@ -277,14 +277,14 @@ export default class Dropdown extends React.PureComponent { } render () { - const { icon, items, size, ariaLabel, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props; + const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props; const open = this.state.id === openDropdownId; return ( <div> <IconButton icon={icon} - title={ariaLabel} + title={title} active={open} disabled={disabled} size={size} diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index 85ee79e11..9754c73dc 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -43,6 +43,7 @@ class Item extends React.PureComponent { onClick: PropTypes.func.isRequired, displayWidth: PropTypes.number, visible: PropTypes.bool.isRequired, + autoplay: PropTypes.bool, }; static defaultProps = { @@ -68,9 +69,13 @@ class Item extends React.PureComponent { } } + getAutoPlay() { + return this.props.autoplay || autoPlayGif; + } + hoverToPlay () { const { attachment } = this.props; - return !autoPlayGif && attachment.get('type') === 'gifv'; + return !this.getAutoPlay() && attachment.get('type') === 'gifv'; } handleClick = (e) => { @@ -222,7 +227,7 @@ class Item extends React.PureComponent { </a> ); } else if (attachment.get('type') === 'gifv') { - const autoPlay = !isIOS() && autoPlayGif; + const autoPlay = !isIOS() && this.getAutoPlay(); thumbnail = ( <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> @@ -271,6 +276,7 @@ class MediaGallery extends React.PureComponent { defaultWidth: PropTypes.number, cacheWidth: PropTypes.func, visible: PropTypes.bool, + autoplay: PropTypes.bool, onToggleVisibility: PropTypes.func, }; @@ -328,7 +334,7 @@ class MediaGallery extends React.PureComponent { } render () { - const { media, intl, sensitive, letterbox, fullwidth, defaultWidth } = this.props; + const { media, intl, sensitive, letterbox, fullwidth, defaultWidth, autoplay } = this.props; const { visible } = this.state; const size = media.take(4).size; const uncached = media.every(attachment => attachment.get('type') === 'unknown'); @@ -350,9 +356,9 @@ class MediaGallery extends React.PureComponent { } if (this.isStandaloneEligible()) { - children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />; + children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />; } else { - children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible || uncached} />); + children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible || uncached} />); } if (uncached) { diff --git a/app/javascript/flavours/glitch/components/relative_timestamp.js b/app/javascript/flavours/glitch/components/relative_timestamp.js index aa4b73cfe..711181dcd 100644 --- a/app/javascript/flavours/glitch/components/relative_timestamp.js +++ b/app/javascript/flavours/glitch/components/relative_timestamp.js @@ -3,6 +3,7 @@ import { injectIntl, defineMessages } from 'react-intl'; import PropTypes from 'prop-types'; const messages = defineMessages({ + today: { id: 'relative_time.today', defaultMessage: 'today' }, just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, @@ -65,12 +66,14 @@ const getUnitDelay = units => { } }; -export const timeAgoString = (intl, date, now, year) => { +export const timeAgoString = (intl, date, now, year, timeGiven = true) => { const delta = now - date.getTime(); let relativeTime; - if (delta < 10 * SECOND) { + if (delta < DAY && !timeGiven) { + relativeTime = intl.formatMessage(messages.today); + } else if (delta < 10 * SECOND) { relativeTime = intl.formatMessage(messages.just_now); } else if (delta < 7 * DAY) { if (delta < MINUTE) { @@ -91,12 +94,14 @@ export const timeAgoString = (intl, date, now, year) => { return relativeTime; }; -const timeRemainingString = (intl, date, now) => { +const timeRemainingString = (intl, date, now, timeGiven = true) => { const delta = date.getTime() - now; let relativeTime; - if (delta < 10 * SECOND) { + if (delta < DAY && !timeGiven) { + relativeTime = intl.formatMessage(messages.today); + } else if (delta < 10 * SECOND) { relativeTime = intl.formatMessage(messages.moments_remaining); } else if (delta < MINUTE) { relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) }); @@ -173,8 +178,9 @@ class RelativeTimestamp extends React.Component { render () { const { timestamp, intl, year, futureDate } = this.props; + const timeGiven = timestamp.includes('T'); const date = new Date(timestamp); - const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year); + const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven); return ( <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}> diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 2c79de4db..a5822866a 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -42,6 +42,14 @@ const isLinkMisleading = (link) => { const linkText = linkTextParts.join(''); const targetURL = new URL(link.href); + if (targetURL.protocol === 'magnet:') { + return !linkText.startsWith('magnet:'); + } + + if (targetURL.protocol === 'xmpp:') { + return !(linkText === targetURL.href || 'xmpp:' + linkText === targetURL.href); + } + // The following may not work with international domain names if (textMatchesTarget(linkText, targetURL.origin, targetURL.host) || textMatchesTarget(linkText.toLowerCase(), targetURL.origin, targetURL.host)) { return false; @@ -120,9 +128,19 @@ export default class StatusContent extends React.PureComponent { if (tagLinks && isLinkMisleading(link)) { // Add a tag besides the link to display its origin + const url = new URL(link.href); const tag = document.createElement('span'); tag.classList.add('link-origin-tag'); - tag.textContent = `[${new URL(link.href).host}]`; + switch (url.protocol) { + case 'xmpp:': + tag.textContent = `[${url.href}]`; + break; + case 'magnet:': + tag.textContent = '(magnet)'; + break; + default: + tag.textContent = `[${url.host}]`; + } link.insertAdjacentText('beforeend', ' '); link.insertAdjacentElement('beforeend', tag); } |