@@ -11,7 +11,7 @@ import { showAlertForError } from './alerts'; | |||
import { showAlert } from './alerts'; | |||
import { defineMessages } from 'react-intl'; | |||
let cancelFetchComposeSuggestionsAccounts; | |||
let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags; | |||
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; | |||
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; | |||
@@ -325,10 +325,12 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => | |||
if (cancelFetchComposeSuggestionsAccounts) { | |||
cancelFetchComposeSuggestionsAccounts(); | |||
} | |||
api(getState).get('/api/v1/accounts/search', { | |||
cancelToken: new CancelToken(cancel => { | |||
cancelFetchComposeSuggestionsAccounts = cancel; | |||
}), | |||
params: { | |||
q: token.slice(1), | |||
resolve: false, | |||
@@ -349,9 +351,30 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { | |||
dispatch(readyComposeSuggestionsEmojis(token, results)); | |||
}; | |||
const fetchComposeSuggestionsTags = (dispatch, getState, token) => { | |||
dispatch(updateSuggestionTags(token)); | |||
}; | |||
const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => { | |||
if (cancelFetchComposeSuggestionsTags) { | |||
cancelFetchComposeSuggestionsTags(); | |||
} | |||
api(getState).get('/api/v2/search', { | |||
cancelToken: new CancelToken(cancel => { | |||
cancelFetchComposeSuggestionsTags = cancel; | |||
}), | |||
params: { | |||
type: 'hashtags', | |||
q: token.slice(1), | |||
resolve: false, | |||
limit: 4, | |||
}, | |||
}).then(({ data }) => { | |||
dispatch(readyComposeSuggestionsTags(token, data.hashtags)); | |||
}).catch(error => { | |||
if (!isCancel(error)) { | |||
dispatch(showAlertForError(error)); | |||
} | |||
}); | |||
}, 200, { leading: true, trailing: true }); | |||
export function fetchComposeSuggestions(token) { | |||
return (dispatch, getState) => { | |||
@@ -385,6 +408,12 @@ export function readyComposeSuggestionsAccounts(token, accounts) { | |||
}; | |||
}; | |||
export const readyComposeSuggestionsTags = (token, tags) => ({ | |||
type: COMPOSE_SUGGESTIONS_READY, | |||
token, | |||
tags, | |||
}); | |||
export function selectComposeSuggestion(position, token, suggestion, path) { | |||
return (dispatch, getState) => { | |||
let completion, startPosition; | |||
@@ -394,8 +423,8 @@ export function selectComposeSuggestion(position, token, suggestion, path) { | |||
startPosition = position - 1; | |||
dispatch(useEmoji(suggestion)); | |||
} else if (suggestion[0] === '#') { | |||
completion = suggestion; | |||
} else if (typeof suggestion === 'object' && suggestion.name) { | |||
completion = `#${suggestion.name}`; | |||
startPosition = position - 1; | |||
} else { | |||
completion = getState().getIn(['accounts', suggestion, 'acct']); | |||
@@ -0,0 +1,28 @@ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import { shortNumberFormat } from 'mastodon/utils/numbers'; | |||
import { FormattedMessage } from 'react-intl'; | |||
export default class AutosuggestHashtag extends React.PureComponent { | |||
static propTypes = { | |||
tag: PropTypes.shape({ | |||
name: PropTypes.string.isRequired, | |||
url: PropTypes.string, | |||
history: PropTypes.array.isRequired, | |||
}).isRequired, | |||
}; | |||
render () { | |||
const { tag } = this.props; | |||
const weeklyUses = shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0)); | |||
return ( | |||
<div className='autosuggest-hashtag'> | |||
<div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div> | |||
<div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -1,6 +1,7 @@ | |||
import React from 'react'; | |||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; | |||
import AutosuggestEmoji from './autosuggest_emoji'; | |||
import AutosuggestHashtag from './autosuggest_hashtag'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import PropTypes from 'prop-types'; | |||
import { isRtl } from '../rtl'; | |||
@@ -167,12 +168,12 @@ export default class AutosuggestInput extends ImmutablePureComponent { | |||
const { selectedSuggestion } = this.state; | |||
let inner, key; | |||
if (typeof suggestion === 'object') { | |||
if (typeof suggestion === 'object' && suggestion.shortcode) { | |||
inner = <AutosuggestEmoji emoji={suggestion} />; | |||
key = suggestion.id; | |||
} else if (suggestion[0] === '#') { | |||
inner = suggestion; | |||
key = suggestion; | |||
} else if (typeof suggestion === 'object' && suggestion.name) { | |||
inner = <AutosuggestHashtag tag={suggestion} />; | |||
key = suggestion.name; | |||
} else { | |||
inner = <AutosuggestAccountContainer id={suggestion} />; | |||
key = suggestion; | |||
@@ -1,6 +1,7 @@ | |||
import React from 'react'; | |||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; | |||
import AutosuggestEmoji from './autosuggest_emoji'; | |||
import AutosuggestHashtag from './autosuggest_hashtag'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import PropTypes from 'prop-types'; | |||
import { isRtl } from '../rtl'; | |||
@@ -173,12 +174,12 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { | |||
const { selectedSuggestion } = this.state; | |||
let inner, key; | |||
if (typeof suggestion === 'object') { | |||
if (typeof suggestion === 'object' && suggestion.shortcode) { | |||
inner = <AutosuggestEmoji emoji={suggestion} />; | |||
key = suggestion.id; | |||
} else if (suggestion[0] === '#') { | |||
inner = suggestion; | |||
key = suggestion; | |||
} else if (typeof suggestion === 'object' && suggestion.name) { | |||
inner = <AutosuggestHashtag tag={suggestion} />; | |||
key = suggestion.name; | |||
} else { | |||
inner = <AutosuggestAccountContainer id={suggestion} />; | |||
key = suggestion; | |||
@@ -17,7 +17,6 @@ import { | |||
COMPOSE_SUGGESTIONS_CLEAR, | |||
COMPOSE_SUGGESTIONS_READY, | |||
COMPOSE_SUGGESTION_SELECT, | |||
COMPOSE_SUGGESTION_TAGS_UPDATE, | |||
COMPOSE_TAG_HISTORY_UPDATE, | |||
COMPOSE_SENSITIVITY_CHANGE, | |||
COMPOSE_SPOILERNESS_CHANGE, | |||
@@ -144,15 +143,20 @@ const insertSuggestion = (state, position, token, completion, path) => { | |||
}); | |||
}; | |||
const updateSuggestionTags = (state, token) => { | |||
const prefix = token.slice(1); | |||
const sortHashtagsByUse = (state, tags) => { | |||
const personalHistory = state.get('tagHistory'); | |||
return state.merge({ | |||
suggestions: state.get('tagHistory') | |||
.filter(tag => tag.toLowerCase().startsWith(prefix.toLowerCase())) | |||
.slice(0, 4) | |||
.map(tag => '#' + tag), | |||
suggestion_token: token, | |||
return tags.sort((a, b) => { | |||
const usedA = personalHistory.includes(a.name); | |||
const usedB = personalHistory.includes(b.name); | |||
if (usedA === usedB) { | |||
return 0; | |||
} else if (usedA && !usedB) { | |||
return 1; | |||
} else { | |||
return -1; | |||
} | |||
}); | |||
}; | |||
@@ -201,6 +205,16 @@ const expiresInFromExpiresAt = expires_at => { | |||
return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600; | |||
}; | |||
const normalizeSuggestions = (state, { accounts, emojis, tags }) => { | |||
if (accounts) { | |||
return accounts.map(item => item.id); | |||
} else if (emojis) { | |||
return emojis; | |||
} else { | |||
return sortHashtagsByUse(state, tags); | |||
} | |||
}; | |||
export default function compose(state = initialState, action) { | |||
switch(action.type) { | |||
case STORE_HYDRATE: | |||
@@ -311,11 +325,9 @@ export default function compose(state = initialState, action) { | |||
case COMPOSE_SUGGESTIONS_CLEAR: | |||
return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null); | |||
case COMPOSE_SUGGESTIONS_READY: | |||
return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token); | |||
return state.set('suggestions', ImmutableList(normalizeSuggestions(state, action))).set('suggestion_token', action.token); | |||
case COMPOSE_SUGGESTION_SELECT: | |||
return insertSuggestion(state, action.position, action.token, action.completion, action.path); | |||
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: | |||
@@ -445,7 +445,8 @@ | |||
} | |||
.autosuggest-account, | |||
.autosuggest-emoji { | |||
.autosuggest-emoji, | |||
.autosuggest-hashtag { | |||
display: flex; | |||
flex-direction: row; | |||
align-items: center; | |||
@@ -454,6 +455,14 @@ | |||
font-size: 14px; | |||
} | |||
.autosuggest-hashtag { | |||
justify-content: space-between; | |||
strong { | |||
font-weight: 500; | |||
} | |||
} | |||
.autosuggest-account-icon, | |||
.autosuggest-emoji img { | |||
display: block; | |||