diff options
Diffstat (limited to 'app/javascript/flavours/glitch')
11 files changed, 134 insertions, 22 deletions
diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 52ad17779..05955963c 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -12,7 +12,7 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { export function searchTextFromRawStatus (status) { const spoilerText = status.spoiler_text || ''; - const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); + const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; } diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.js index ec2fbbe4b..1ce2f42b4 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.js +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.js @@ -208,7 +208,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { <span style={{ display: 'none' }}>{placeholder}</span> <Textarea - inputRef={this.setTextarea} + ref={this.setTextarea} className='autosuggest-textarea__textarea' disabled={disabled} placeholder={placeholder} diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 91bc06b3c..e036c0da7 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -656,6 +656,7 @@ class Status extends ImmutablePureComponent { compact cacheWidth={this.props.cacheMediaWidth} defaultWidth={this.props.cachedMediaWidth} + sensitive={status.get('sensitive')} /> ); mediaIcon = 'link'; diff --git a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js index 0bb71e872..abc3f468f 100644 --- a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js +++ b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js @@ -106,6 +106,10 @@ class KeyboardShortcuts extends ImmutablePureComponent { <td><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a brand new toot' /></td> </tr> <tr> + <td><kbd>alt</kbd>+<kbd>x</kbd></td> + <td><FormattedMessage id='keyboard_shortcuts.spoilers' defaultMessage='to show/hide CW field' /></td> + </tr> + <tr> <td><kbd>backspace</kbd></td> <td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td> </tr> diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js index e3ee7dada..03867e03a 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.js +++ b/app/javascript/flavours/glitch/features/status/components/card.js @@ -2,10 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import Immutable from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage } from 'react-intl'; import punycode from 'punycode'; import classnames from 'classnames'; import { decode as decodeIDNA } from 'flavours/glitch/util/idna'; import Icon from 'flavours/glitch/components/icon'; +import classNames from 'classnames'; +import { useBlurhash } from 'flavours/glitch/util/initial_state'; +import { decode } from 'blurhash'; const getHostname = url => { const parser = document.createElement('a'); @@ -55,6 +59,7 @@ export default class Card extends React.PureComponent { compact: PropTypes.bool, defaultWidth: PropTypes.number, cacheWidth: PropTypes.func, + sensitive: PropTypes.bool, }; static defaultProps = { @@ -64,12 +69,44 @@ export default class Card extends React.PureComponent { state = { width: this.props.defaultWidth || 280, + previewLoaded: false, embedded: false, + revealed: !this.props.sensitive, }; componentWillReceiveProps (nextProps) { if (!Immutable.is(this.props.card, nextProps.card)) { - this.setState({ embedded: false }); + this.setState({ embedded: false, previewLoaded: false }); + } + if (this.props.sensitive !== nextProps.sensitive) { + this.setState({ revealed: !nextProps.sensitive }); + } + } + + componentDidMount () { + if (this.props.card && this.props.card.get('blurhash')) { + this._decode(); + } + } + + componentDidUpdate (prevProps) { + const { card } = this.props; + if (card.get('blurhash') && (!prevProps.card || prevProps.card.get('blurhash') !== card.get('blurhash'))) { + this._decode(); + } + } + + _decode () { + if (!useBlurhash) return; + + const hash = this.props.card.get('blurhash'); + const pixels = decode(hash, 32, 32); + + if (pixels) { + const ctx = this.canvas.getContext('2d'); + const imageData = new ImageData(pixels, 32, 32); + + ctx.putImageData(imageData, 0, 0); } } @@ -111,6 +148,18 @@ export default class Card extends React.PureComponent { } } + setCanvasRef = c => { + this.canvas = c; + } + + handleImageLoad = () => { + this.setState({ previewLoaded: true }); + } + + handleReveal = () => { + this.setState({ revealed: true }); + } + renderVideo () { const { card } = this.props; const content = { __html: addAutoPlay(card.get('html')) }; @@ -130,7 +179,7 @@ export default class Card extends React.PureComponent { render () { const { card, maxDescription, compact, defaultWidth } = this.props; - const { width, embedded } = this.state; + const { width, embedded, revealed } = this.state; if (card === null) { return null; @@ -145,7 +194,7 @@ export default class Card extends React.PureComponent { const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); const description = ( - <div className='status-card__content'> + <div className={classNames('status-card__content', { 'status-card__content--blurred': !revealed })}> {title} {!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>} <span className='status-card__host'>{provider}</span> @@ -153,7 +202,18 @@ export default class Card extends React.PureComponent { ); let embed = ''; - let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />; + let canvas = <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('status-card__image-preview', { 'status-card__image-preview--hidden' : revealed && this.state.previewLoaded })} />; + let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />; + let spoilerButton = ( + <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'> + <span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> + </button> + ); + spoilerButton = ( + <div className={classNames('spoiler-button', { 'spoiler-button--minified': revealed })}> + {spoilerButton} + </div> + ); if (interactive) { if (embedded) { @@ -167,14 +227,18 @@ export default class Card extends React.PureComponent { embed = ( <div className='status-card__image'> + {canvas} {thumbnail} - <div className='status-card__actions'> - <div> - <button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button> - {horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>} + {revealed && ( + <div className='status-card__actions'> + <div> + <button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button> + {horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>} + </div> </div> - </div> + )} + {!revealed && spoilerButton} </div> ); } @@ -188,13 +252,16 @@ export default class Card extends React.PureComponent { } else if (card.get('image')) { embed = ( <div className='status-card__image'> + {canvas} {thumbnail} + {!revealed && spoilerButton} </div> ); } else { embed = ( <div className='status-card__image'> <Icon id='file-text' /> + {!revealed && spoilerButton} </div> ); } diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 17f22a8a2..4fbd65517 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -184,7 +184,7 @@ export default class DetailedStatus extends ImmutablePureComponent { mediaIcon = 'picture-o'; } } else if (status.get('card')) { - media = <Card onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />; + media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />; mediaIcon = 'link'; } diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index f8f6cff88..bf76c0e57 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; import { Redirect, withRouter } from 'react-router-dom'; import { isMobile } from 'flavours/glitch/util/is_mobile'; import { debounce } from 'lodash'; -import { uploadCompose, resetCompose } from 'flavours/glitch/actions/compose'; +import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose'; import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications'; import { fetchFilters } from 'flavours/glitch/actions/filters'; @@ -81,6 +81,7 @@ const keyMap = { new: 'n', search: 's', forceNew: 'option+n', + toggleComposeSpoilers: 'option+x', focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'], reply: 'r', favourite: 'f', @@ -396,7 +397,7 @@ class UI extends React.Component { componentDidMount () { this.hotkeys.__mousetrap__.stopCallback = (e, element) => { - return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName); + return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName) && !e.altKey; }; } @@ -455,6 +456,11 @@ class UI extends React.Component { this.props.dispatch(resetCompose()); } + handleHotkeyToggleComposeSpoilers = e => { + e.preventDefault(); + this.props.dispatch(changeComposeSpoilerness()); + } + handleHotkeyFocusColumn = e => { const index = (e.key * 1) + 1; // First child is drawer, skip that const column = this.node.querySelector(`.column:nth-child(${index})`); @@ -569,6 +575,7 @@ class UI extends React.Component { new: this.handleHotkeyNew, search: this.handleHotkeySearch, forceNew: this.handleHotkeyForceNew, + toggleComposeSpoilers: this.handleHotkeyToggleComposeSpoilers, focusColumn: this.handleHotkeyFocusColumn, back: this.handleHotkeyBack, goToHome: this.handleHotkeyGoToHome, diff --git a/app/javascript/flavours/glitch/styles/accessibility.scss b/app/javascript/flavours/glitch/styles/accessibility.scss index 35e91da80..1a2de2f06 100644 --- a/app/javascript/flavours/glitch/styles/accessibility.scss +++ b/app/javascript/flavours/glitch/styles/accessibility.scss @@ -1,13 +1,13 @@ -$emojis-requiring-outlines: '8ball' 'ant' 'back' 'black_circle' 'black_heart' 'black_large_square' 'black_medium_small_square' 'black_medium_square' 'black_nib' 'black_small_square' 'bomb' 'bowling' 'bust_in_silhouette' 'busts_in_silhouette' 'camera' 'camera_with_flash' 'clubs' 'copyright' 'curly_loop' 'currency_exchange' 'dark_sunglasses' 'eight_pointed_black_star' 'electric_plug' 'end' 'female-guard' 'film_projector' 'fried_egg' 'gorilla' 'guardsman' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'hocho' 'hole' 'joystick' 'kaaba' 'lower_left_ballpoint_pen' 'lower_left_fountain_pen' 'male-guard' 'microphone' 'mortar_board' 'movie_camera' 'musical_score' 'on' 'registered' 'soon' 'spades' 'speaking_head_in_silhouette' 'spider' 'telephone_receiver' 'tm' 'top' 'tophat' 'turkey' 'vhs' 'video_camera' 'video_game' 'water_buffalo' 'waving_black_flag' 'wavy_dash' !default; +$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange' 'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on' 'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default; -%emoji-outline { - filter: drop-shadow(1px 1px 0 $primary-text-color) drop-shadow(-1px 1px 0 $primary-text-color) drop-shadow(1px -1px 0 $primary-text-color) drop-shadow(-1px -1px 0 $primary-text-color); +%emoji-color-inversion { + filter: invert(1); } .emojione { - @each $emoji in $emojis-requiring-outlines { + @each $emoji in $emojis-requiring-inversion { &[title=':#{$emoji}:'] { - @extend %emoji-outline; + @extend %emoji-color-inversion; } } } diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index 50b7f2a72..28a4ce0ce 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -874,6 +874,11 @@ a.status-card { flex: 1 1 auto; overflow: hidden; padding: 14px 14px 14px 8px; + + &--blurred { + filter: blur(2px); + pointer-events: none; + } } .status-card__description { @@ -911,7 +916,8 @@ a.status-card { width: 100%; } - .status-card__image-image { + .status-card__image-image, + .status-card__image-preview { border-radius: 4px 4px 0 0; } @@ -956,6 +962,24 @@ a.status-card.compact:hover { background-position: center center; } +.status-card__image-preview { + border-radius: 4px 0 0 4px; + display: block; + margin: 0; + width: 100%; + height: 100%; + object-fit: fill; + position: absolute; + top: 0; + left: 0; + z-index: 0; + background: $base-overlay-background; + + &--hidden { + display: none; + } +} + .attachment-list { display: flex; font-size: 14px; diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss index 312f5e314..7709d4535 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss @@ -37,4 +37,4 @@ $account-background-color: $white !default; @return hsl(hue($color), saturation($color), lightness($color) - $amount); } -$emojis-requiring-outlines: 'alien' 'baseball' 'chains' 'chicken' 'cloud' 'crescent_moon' 'dash' 'dove_of_peace' 'eyes' 'first_quarter_moon' 'first_quarter_moon_with_face' 'fish_cake' 'full_moon' 'full_moon_with_face' 'ghost' 'goat' 'grey_exclamation' 'grey_question' 'ice_skate' 'last_quarter_moon' 'last_quarter_moon_with_face' 'lightning' 'loud_sound' 'moon' 'mute' 'page_with_curl' 'rain_cloud' 'ram' 'rice' 'rice_ball' 'rooster' 'sheep' 'skull' 'skull_and_crossbones' 'snow_cloud' 'sound' 'speaker' 'speech_balloon' 'thought_balloon' 'volleyball' 'waning_crescent_moon' 'waning_gibbous_moon' 'waving_white_flag' 'waxing_crescent_moon' 'white_circle' 'white_large_square' 'white_medium_small_square' 'white_medium_square' 'white_small_square' 'wind_blowing_face'; +$emojis-requiring-inversion: 'chains'; diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js index e1a244127..22b657b05 100644 --- a/app/javascript/flavours/glitch/util/emoji/index.js +++ b/app/javascript/flavours/glitch/util/emoji/index.js @@ -6,6 +6,15 @@ const trie = new Trie(Object.keys(unicodeMapping)); const assetHost = process.env.CDN_HOST || ''; +// Emoji requiring extra borders depending on theme +const darkEmoji = '🎱🐜⚫🖤⬛◼️◾◼️✒️▪️💣🎳📷📸♣️🕶️✴️🔌💂♀️📽️🍳🦍💂🔪🕳️🕹️🕋🖊️🖋️💂♂️🎤🎓🎥🎼♠️🎩🦃📼📹🎮🐃🏴'; +const lightEmoji = '👽⚾🐔☁️💨🕊️👀🍥👻🐐❕❔⛸️🌩️🔊🔇📃🌧️🐏🍚🍙🐓🐑💀☠️🌨️🔉🔈💬💭🏐🏳️⚪⬜◽◻️▫️'; + +const emojiFilename = (filename, match) => { + const borderedEmoji = (document.body && document.body.classList.contains('skin-mastodon-light')) ? lightEmoji : darkEmoji; + return borderedEmoji.includes(match) ? (filename + '_border') : filename; +}; + const emojify = (str, customEmojis = {}) => { const tagCharsWithoutEmojis = '<&'; const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&'; @@ -60,7 +69,7 @@ const emojify = (str, customEmojis = {}) => { } else if (!useSystemEmojiFont) { // matched to unicode emoji const { filename, shortCode } = unicodeMapping[match]; const title = shortCode ? `:${shortCode}:` : ''; - replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`; + replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${emojiFilename(filename, match)}.svg" />`; rend = i + match.length; // If the matched character was followed by VS15 (for selecting text presentation), skip it. if (str.codePointAt(rend) === 65038) { |