[Glitch] Implement tag auto-completion by history
Port 460e380d38
to glitch-soc
This commit is contained in:
parent
e3246cd13b
commit
24b6811a6e
|
@ -3,6 +3,7 @@ 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 resizeImage from 'flavours/glitch/util/resize_image';
|
||||
|
||||
import { updateTimeline } from './timelines';
|
||||
|
@ -28,6 +29,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 +140,7 @@ export function submitCompose() {
|
|||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
},
|
||||
}).then(function (response) {
|
||||
dispatch(insertIntoTagHistory(response.data.tags));
|
||||
dispatch(submitComposeSuccess({ ...response.data }));
|
||||
|
||||
// If the response has no data then we can't do anything else.
|
||||
|
@ -315,12 +320,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 +358,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 +377,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(tags) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const oldHistory = state.getIn(['compose', 'tagHistory']);
|
||||
const me = state.getIn(['meta', 'me']);
|
||||
const names = tags.map(({ name }) => name);
|
||||
const intersectedOldHistory = oldHistory.filter(name => !names.includes(name));
|
||||
|
||||
names.push(...intersectedOldHistory.toJS());
|
||||
|
||||
const newHistory = names.slice(0, 1000);
|
||||
|
||||
tagHistory.set(me, newHistory);
|
||||
dispatch(updateTagHistory(newHistory));
|
||||
};
|
||||
}
|
||||
|
||||
export function mountCompose() {
|
||||
return {
|
||||
type: COMPOSE_MOUNT,
|
||||
|
|
|
@ -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) {
|
||||
return dispatch => {
|
||||
const state = convertState(rawState);
|
||||
|
||||
return {
|
||||
dispatch({
|
||||
type: STORE_HYDRATE,
|
||||
state,
|
||||
});
|
||||
|
||||
dispatch(hydrateCompose());
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -77,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)',
|
||||
|
@ -206,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.startsWith(prefix))
|
||||
.slice(0, 4)
|
||||
.map(tag => '#' + tag),
|
||||
suggestion_token: token,
|
||||
});
|
||||
};
|
||||
|
||||
const insertEmoji = (state, position, emojiData) => {
|
||||
const emoji = emojiData.native;
|
||||
|
||||
|
@ -360,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);
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in New Issue