diff options
author | pluralcafe-docker <docker@plural.cafe> | 2018-08-30 05:23:58 +0000 |
---|---|---|
committer | pluralcafe-docker <docker@plural.cafe> | 2018-08-30 05:23:58 +0000 |
commit | cc7437e25597e24b9a5f06f7991861506d9abe5c (patch) | |
tree | e627d32df29ef7ae30a67607caf3ecdc1ae333a9 /app/javascript/flavours | |
parent | 395164add468b1079669699dfe8eeaab73f69c15 (diff) | |
parent | 5ce67276691c37baad149f2f89f765543f70e6f9 (diff) |
Merge branch 'glitch'
Diffstat (limited to 'app/javascript/flavours')
19 files changed, 183 insertions, 55 deletions
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index f6c8086fe..fb311fc0a 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -3,6 +3,8 @@ import { CancelToken } from 'axios'; import { throttle } from 'lodash'; import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light'; import { useEmoji } from './emojis'; +import { tagHistory } from 'flavours/glitch/util/settings'; +import { recoverHashtags } from 'flavours/glitch/util/hashtag'; import resizeImage from 'flavours/glitch/util/resize_image'; import { updateTimeline } from './timelines'; @@ -28,6 +30,9 @@ export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; +export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE'; + +export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE'; export const COMPOSE_MOUNT = 'COMPOSE_MOUNT'; export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT'; @@ -136,6 +141,7 @@ export function submitCompose() { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), }, }).then(function (response) { + dispatch(insertIntoTagHistory(response.data.tags, status)); dispatch(submitComposeSuccess({ ...response.data })); // If the response has no data then we can't do anything else. @@ -315,12 +321,22 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { dispatch(readyComposeSuggestionsEmojis(token, results)); }; +const fetchComposeSuggestionsTags = (dispatch, getState, token) => { + dispatch(updateSuggestionTags(token)); +}; + export function fetchComposeSuggestions(token) { return (dispatch, getState) => { - if (token[0] === ':') { + switch (token[0]) { + case ':': fetchComposeSuggestionsEmojis(dispatch, getState, token); - } else { + break; + case '#': + fetchComposeSuggestionsTags(dispatch, getState, token); + break; + default: fetchComposeSuggestionsAccounts(dispatch, getState, token); + break; } }; }; @@ -343,10 +359,15 @@ export function readyComposeSuggestionsAccounts(token, accounts) { export function selectComposeSuggestion(position, token, suggestion) { return (dispatch, getState) => { - const completion = typeof suggestion === 'object' && suggestion.id ? ( - dispatch(useEmoji(suggestion)), - suggestion.native || suggestion.colons - ) : '@' + getState().getIn(['accounts', suggestion, 'acct']); + let completion; + if (typeof suggestion === 'object' && suggestion.id) { + dispatch(useEmoji(suggestion)); + completion = suggestion.native || suggestion.colons; + } else if (suggestion[0] === '#') { + completion = suggestion; + } else { + completion = '@' + getState().getIn(['accounts', suggestion, 'acct']); + } dispatch({ type: COMPOSE_SUGGESTION_SELECT, @@ -357,6 +378,48 @@ export function selectComposeSuggestion(position, token, suggestion) { }; }; +export function updateSuggestionTags(token) { + return { + type: COMPOSE_SUGGESTION_TAGS_UPDATE, + token, + }; +} + +export function updateTagHistory(tags) { + return { + type: COMPOSE_TAG_HISTORY_UPDATE, + tags, + }; +} + +export function hydrateCompose() { + return (dispatch, getState) => { + const me = getState().getIn(['meta', 'me']); + const history = tagHistory.get(me); + + if (history !== null) { + dispatch(updateTagHistory(history)); + } + }; +} + +function insertIntoTagHistory(recognizedTags, text) { + return (dispatch, getState) => { + const state = getState(); + const oldHistory = state.getIn(['compose', 'tagHistory']); + const me = state.getIn(['meta', 'me']); + const names = recoverHashtags(recognizedTags, text); + const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1); + + names.push(...intersectedOldHistory.toJS()); + + const newHistory = names.slice(0, 1000); + + tagHistory.set(me, newHistory); + dispatch(updateTagHistory(newHistory)); + }; +} + export function mountCompose() { return { type: COMPOSE_MOUNT, diff --git a/app/javascript/flavours/glitch/actions/store.js b/app/javascript/flavours/glitch/actions/store.js index a1db0fdd5..2dd94a998 100644 --- a/app/javascript/flavours/glitch/actions/store.js +++ b/app/javascript/flavours/glitch/actions/store.js @@ -1,4 +1,5 @@ import { Iterable, fromJS } from 'immutable'; +import { hydrateCompose } from './compose'; export const STORE_HYDRATE = 'STORE_HYDRATE'; export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; @@ -8,10 +9,14 @@ const convertState = rawState => Iterable.isIndexed(v) ? v.toList() : v.toMap()); export function hydrateStore(rawState) { - const state = convertState(rawState); + return dispatch => { + const state = convertState(rawState); - return { - type: STORE_HYDRATE, - state, + dispatch({ + type: STORE_HYDRATE, + state, + }); + + dispatch(hydrateCompose()); }; }; diff --git a/app/javascript/flavours/glitch/components/extended_video_player.js b/app/javascript/flavours/glitch/components/extended_video_player.js index 9e2f6835a..009c0d559 100644 --- a/app/javascript/flavours/glitch/components/extended_video_player.js +++ b/app/javascript/flavours/glitch/components/extended_video_player.js @@ -50,6 +50,7 @@ export default class ExtendedVideoPlayer extends React.PureComponent { role='button' tabIndex='0' aria-label={alt} + title={alt} muted={muted} controls={controls} loop={!controls} diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index d87be14cc..3faf0b453 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -174,6 +174,7 @@ class Item extends React.PureComponent { <video className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`} aria-label={attachment.get('description')} + title={attachment.get('description')} role='application' src={attachment.get('url')} onClick={this.handleClick} diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index a87721ef8..1ac5a4b3e 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -412,6 +412,7 @@ export default class Status extends ImmutablePureComponent { {Component => (<Component preview={video.get('preview_url')} src={video.get('url')} + alt={video.get('description')} inline sensitive={status.get('sensitive')} letterbox={settings.getIn(['media', 'letterbox'])} @@ -474,6 +475,7 @@ export default class Status extends ImmutablePureComponent { const computedClass = classNames('status', `status-${status.get('visibility')}`, { collapsed: isCollapsed, 'has-background': isCollapsed && background, + 'status__wrapper-reply': !!status.get('in_reply_to_id'), muted, }, 'focusable'); diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 174df0cc9..eda0d637e 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -56,7 +56,9 @@ export default class Header extends ImmutablePureComponent { } if (me !== account.get('id')) { - if (account.getIn(['relationship', 'requested'])) { + if (!account.get('relationship')) { // Wait until the relationship is loaded + actionBtn = ''; + } else if (account.getIn(['relationship', 'requested'])) { actionBtn = ( <div className='account--action-button'> <IconButton size={26} active icon='hourglass' title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} /> diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js index f312e9d59..cf6f45b34 100644 --- a/app/javascript/flavours/glitch/features/composer/index.js +++ b/app/javascript/flavours/glitch/features/composer/index.js @@ -51,6 +51,7 @@ import { privacyPreference } from 'flavours/glitch/util/privacy_preference'; // State mapping. function mapStateToProps (state) { + const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']); const inReplyTo = state.getIn(['compose', 'in_reply_to']); const replyPrivacy = inReplyTo ? state.getIn(['statuses', inReplyTo, 'visibility']) : null; const sideArmBasePrivacy = state.getIn(['local_settings', 'side_arm']); @@ -85,12 +86,13 @@ function mapStateToProps (state) { sideArm: sideArmPrivacy, sensitive: state.getIn(['compose', 'sensitive']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), - spoiler: state.getIn(['compose', 'spoiler']), + spoiler: spoilersAlwaysOn || state.getIn(['compose', 'spoiler']), spoilerText: state.getIn(['compose', 'spoiler_text']), suggestionToken: state.getIn(['compose', 'suggestion_token']), suggestions: state.getIn(['compose', 'suggestions']), text: state.getIn(['compose', 'text']), anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, + spoilersAlwaysOn: spoilersAlwaysOn, }; }; @@ -376,6 +378,7 @@ class Composer extends React.Component { spoilerText, suggestions, text, + spoilersAlwaysOn, } = this.props; let disabledButton = isSubmitting || isUploading || (!!text.length && !text.trim().length && !anyMedia); @@ -443,7 +446,7 @@ class Composer extends React.Component { onDoodleOpen={onOpenDoodleModal} onModalClose={onCloseModal} onModalOpen={onOpenActionsModal} - onToggleSpoiler={onChangeSpoilerness} + onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness} onUpload={onUpload} privacy={privacy} resetFileKey={resetFileKey} @@ -515,6 +518,7 @@ Composer.propTypes = { onUnmount: PropTypes.func, onUpload: PropTypes.func, anyMedia: PropTypes.bool, + spoilersAlwaysOn: PropTypes.bool, }; // Connecting and export. diff --git a/app/javascript/flavours/glitch/features/composer/options/index.js b/app/javascript/flavours/glitch/features/composer/options/index.js index c129622bc..05cbe24c9 100644 --- a/app/javascript/flavours/glitch/features/composer/options/index.js +++ b/app/javascript/flavours/glitch/features/composer/options/index.js @@ -285,13 +285,15 @@ export default class ComposerOptions extends React.PureComponent { title={intl.formatMessage(messages.change_privacy)} value={privacy} /> - <TextIconButton - active={spoiler} - ariaControls='glitch.composer.spoiler.input' - label='CW' - onClick={onToggleSpoiler} - title={intl.formatMessage(messages.spoiler)} - /> + {onToggleSpoiler && ( + <TextIconButton + active={spoiler} + ariaControls='glitch.composer.spoiler.input' + label='CW' + onClick={onToggleSpoiler} + title={intl.formatMessage(messages.spoiler)} + /> + )} <Dropdown active={advancedOptions && advancedOptions.some(value => !!value)} disabled={disabled} diff --git a/app/javascript/flavours/glitch/features/composer/textarea/index.js b/app/javascript/flavours/glitch/features/composer/textarea/index.js index 51d44a83b..50e46fc78 100644 --- a/app/javascript/flavours/glitch/features/composer/textarea/index.js +++ b/app/javascript/flavours/glitch/features/composer/textarea/index.js @@ -58,7 +58,7 @@ const handlers = { const right = value.slice(selectionStart).search(/[\s\u200B]/); const token = function () { switch (true) { - case left < 0 || !/[@:]/.test(value[left]): + case left < 0 || !/[@:#]/.test(value[left]): return null; case right < 0: return value.slice(left); diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js index f55640bcf..331692398 100644 --- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js +++ b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js @@ -57,6 +57,42 @@ export default class ComposerTextareaSuggestionsItem extends React.Component { } = this.props; const computedClass = classNames('composer--textarea--suggestions--item', { selected }); + // If the suggestion is an object, then we render an emoji. + // Otherwise, we render a hashtag if it starts with #, or an account. + let inner; + if (typeof suggestion === 'object') { + let url; + if (suggestion.custom) { + url = suggestion.imageUrl; + } else { + const mapping = unicodeMapping[suggestion.native] || unicodeMapping[suggestion.native.replace(/\uFE0F$/, '')]; + if (mapping) { + url = `${assetHost}/emoji/${mapping.filename}.svg`; + } + } + if (url) { + inner = ( + <div className='emoji'> + <img + alt={suggestion.native || suggestion.colons} + className='emojione' + src={url} + /> + {suggestion.colons} + </div> + ); + } + } else if (suggestion[0] === '#') { + inner = suggestion; + } else { + inner = ( + <AccountContainer + id={suggestion} + small + /> + ); + } + // The result. return ( <div @@ -66,37 +102,7 @@ export default class ComposerTextareaSuggestionsItem extends React.Component { role='button' tabIndex='0' > - { // If the suggestion is an object, then we render an emoji. - // Otherwise, we render an account. - typeof suggestion === 'object' ? function () { - const url = function () { - if (suggestion.custom) { - return suggestion.imageUrl; - } else { - const mapping = unicodeMapping[suggestion.native] || unicodeMapping[suggestion.native.replace(/\uFE0F$/, '')]; - if (!mapping) { - return null; - } - return `${assetHost}/emoji/${mapping.filename}.svg`; - } - }(); - return url ? ( - <div className='emoji'> - <img - alt={suggestion.native || suggestion.colons} - className='emojione' - src={url} - /> - {suggestion.colons} - </div> - ) : null; - }() : ( - <AccountContainer - id={suggestion} - small - /> - ) - } + { inner } </div> ); } diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js index f88e23c47..ad5c11979 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -77,6 +77,14 @@ export default class LocalSettingsPage extends React.PureComponent { <h2><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box options' /></h2> <LocalSettingsPageItem settings={settings} + item={['always_show_spoilers_field']} + id='mastodon-settings--always_show_spoilers_field' + onChange={onChange} + > + <FormattedMessage id='settings.always_show_spoilers_field' defaultMessage='Always enable the Content Warning field' /> + </LocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} item={['side_arm']} id='mastodon-settings--side_arm' options={[ diff --git a/app/javascript/flavours/glitch/features/report/components/status_check_box.js b/app/javascript/flavours/glitch/features/report/components/status_check_box.js index a685132b0..d674eecf9 100644 --- a/app/javascript/flavours/glitch/features/report/components/status_check_box.js +++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.js @@ -36,6 +36,7 @@ export default class StatusCheckBox extends React.PureComponent { <Component preview={video.get('preview_url')} src={video.get('url')} + alt={video.get('description')} width={239} height={110} inline 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 5cfc9dfae..ee9eb02c6 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -60,6 +60,7 @@ export default class DetailedStatus extends ImmutablePureComponent { <Video preview={video.get('preview_url')} src={video.get('url')} + alt={video.get('description')} inline sensitive={status.get('sensitive')} letterbox={settings.getIn(['media', 'letterbox'])} diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index a578df026..7e284a0bc 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -323,6 +323,7 @@ export default class Video extends React.PureComponent { role='button' tabIndex='0' aria-label={alt} + title={alt} width={width} height={height} onClick={this.togglePlay} diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 8b997bf4d..594d70ee2 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -18,6 +18,8 @@ import { COMPOSE_SUGGESTIONS_CLEAR, COMPOSE_SUGGESTIONS_READY, COMPOSE_SUGGESTION_SELECT, + COMPOSE_SUGGESTION_TAGS_UPDATE, + COMPOSE_TAG_HISTORY_UPDATE, COMPOSE_ADVANCED_OPTIONS_CHANGE, COMPOSE_SENSITIVITY_CHANGE, COMPOSE_SPOILERNESS_CHANGE, @@ -39,6 +41,7 @@ import { privacyPreference } from 'flavours/glitch/util/privacy_preference'; import { me } from 'flavours/glitch/util/initial_state'; import { overwrite } from 'flavours/glitch/util/js_helpers'; import { unescapeHTML } from 'flavours/glitch/util/html'; +import { recoverHashtags } from 'flavours/glitch/util/hashtag'; const totalElefriends = 3; @@ -76,6 +79,7 @@ const initialState = ImmutableMap({ default_sensitive: false, resetFileKey: Math.floor((Math.random() * 0x10000)), idempotencyKey: null, + tagHistory: ImmutableList(), doodle: ImmutableMap({ fg: 'rgb( 0, 0, 0)', bg: 'rgb(255, 255, 255)', @@ -114,8 +118,9 @@ function apiStatusToTextMentions (state, status) { } function apiStatusToTextHashtags (state, status) { - return ImmutableOrderedSet([]).union(status.tags.map( - ({ name }) => `#${name} ` + const text = unescapeHTML(status.content); + return ImmutableOrderedSet([]).union(recoverHashtags(status.tags, text).map( + (name) => `#${name} ` )).join(''); } @@ -204,6 +209,18 @@ const insertSuggestion = (state, position, token, completion) => { }); }; +const updateSuggestionTags = (state, token) => { + const prefix = token.slice(1); + + return state.merge({ + suggestions: state.get('tagHistory') + .filter(tag => tag.toLowerCase().startsWith(prefix.toLowerCase())) + .slice(0, 4) + .map(tag => '#' + tag), + suggestion_token: token, + }); +}; + const insertEmoji = (state, position, emojiData) => { const emoji = emojiData.native; @@ -358,6 +375,10 @@ export default function compose(state = initialState, action) { return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token); case COMPOSE_SUGGESTION_SELECT: return insertSuggestion(state, action.position, action.token, action.completion); + case COMPOSE_SUGGESTION_TAGS_UPDATE: + return updateSuggestionTags(state, action.token); + case COMPOSE_TAG_HISTORY_UPDATE: + return state.set('tagHistory', fromJS(action.tags)); case TIMELINE_DELETE: if (action.id === state.get('in_reply_to')) { return state.set('in_reply_to', null); diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index 51032f345..1d24f0e9a 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -12,6 +12,7 @@ const initialState = ImmutableMap({ side_arm : 'none', side_arm_reply_mode : 'keep', show_reply_count : false, + always_show_spoilers_field: false, collapsed : ImmutableMap({ enabled : true, auto : ImmutableMap({ diff --git a/app/javascript/flavours/glitch/styles/components/metadata.scss b/app/javascript/flavours/glitch/styles/components/metadata.scss index 29a6330e9..2efe6cd66 100644 --- a/app/javascript/flavours/glitch/styles/components/metadata.scss +++ b/app/javascript/flavours/glitch/styles/components/metadata.scss @@ -21,7 +21,7 @@ dt, dd { box-sizing: border-box; - padding: 14px 20px; + padding: 14px 5px; text-align: center; max-height: 48px; overflow: hidden; diff --git a/app/javascript/flavours/glitch/util/hashtag.js b/app/javascript/flavours/glitch/util/hashtag.js new file mode 100644 index 000000000..d5ea57662 --- /dev/null +++ b/app/javascript/flavours/glitch/util/hashtag.js @@ -0,0 +1,8 @@ +export function recoverHashtags (recognizedTags, text) { + return recognizedTags.map(tag => { + const re = new RegExp(`(?:^|[^\/\)\w])#(${tag.name})`, 'i'); + const matched_hashtag = text.match(re); + return matched_hashtag ? matched_hashtag[1] : tag; + } + ); +} diff --git a/app/javascript/flavours/glitch/util/settings.js b/app/javascript/flavours/glitch/util/settings.js index dbd969cb1..7643a508e 100644 --- a/app/javascript/flavours/glitch/util/settings.js +++ b/app/javascript/flavours/glitch/util/settings.js @@ -44,3 +44,4 @@ export default class Settings { } export const pushNotificationsSetting = new Settings('mastodon_push_notification_data'); +export const tagHistory = new Settings('mastodon_tag_history'); |