Implement tag auto-completion by history (#6621)
This is a functionality similar to one implemented in Pawoo:
21a3c70f80
gh/stable
parent
778b37790b
commit
460e380d38
|
@ -1,6 +1,7 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
|
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
|
||||||
|
import { tagHistory } from '../settings';
|
||||||
import { useEmoji } from './emojis';
|
import { useEmoji } from './emojis';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -27,6 +28,9 @@ export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
||||||
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
||||||
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
||||||
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
|
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_MOUNT = 'COMPOSE_MOUNT';
|
||||||
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
|
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
|
||||||
|
@ -111,6 +115,7 @@ export function submitCompose() {
|
||||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||||
},
|
},
|
||||||
}).then(function (response) {
|
}).then(function (response) {
|
||||||
|
dispatch(insertIntoTagHistory(response.data.tags));
|
||||||
dispatch(submitComposeSuccess({ ...response.data }));
|
dispatch(submitComposeSuccess({ ...response.data }));
|
||||||
|
|
||||||
// To make the app more responsive, immediately get the status into the columns
|
// To make the app more responsive, immediately get the status into the columns
|
||||||
|
@ -273,12 +278,22 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
|
||||||
dispatch(readyComposeSuggestionsEmojis(token, results));
|
dispatch(readyComposeSuggestionsEmojis(token, results));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchComposeSuggestionsTags = (dispatch, getState, token) => {
|
||||||
|
dispatch(updateSuggestionTags(token));
|
||||||
|
};
|
||||||
|
|
||||||
export function fetchComposeSuggestions(token) {
|
export function fetchComposeSuggestions(token) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (token[0] === ':') {
|
switch (token[0]) {
|
||||||
|
case ':':
|
||||||
fetchComposeSuggestionsEmojis(dispatch, getState, token);
|
fetchComposeSuggestionsEmojis(dispatch, getState, token);
|
||||||
} else {
|
break;
|
||||||
|
case '#':
|
||||||
|
fetchComposeSuggestionsTags(dispatch, getState, token);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
fetchComposeSuggestionsAccounts(dispatch, getState, token);
|
fetchComposeSuggestionsAccounts(dispatch, getState, token);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -308,6 +323,9 @@ export function selectComposeSuggestion(position, token, suggestion) {
|
||||||
startPosition = position - 1;
|
startPosition = position - 1;
|
||||||
|
|
||||||
dispatch(useEmoji(suggestion));
|
dispatch(useEmoji(suggestion));
|
||||||
|
} else if (suggestion[0] === '#') {
|
||||||
|
completion = suggestion;
|
||||||
|
startPosition = position - 1;
|
||||||
} else {
|
} else {
|
||||||
completion = getState().getIn(['accounts', suggestion, 'acct']);
|
completion = getState().getIn(['accounts', suggestion, 'acct']);
|
||||||
startPosition = position;
|
startPosition = position;
|
||||||
|
@ -322,6 +340,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() {
|
export function mountCompose() {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_MOUNT,
|
type: COMPOSE_MOUNT,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Iterable, fromJS } from 'immutable';
|
import { Iterable, fromJS } from 'immutable';
|
||||||
|
import { hydrateCompose } from './compose';
|
||||||
|
|
||||||
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
||||||
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
|
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
|
||||||
|
@ -8,10 +9,14 @@ const convertState = rawState =>
|
||||||
Iterable.isIndexed(v) ? v.toList() : v.toMap());
|
Iterable.isIndexed(v) ? v.toList() : v.toMap());
|
||||||
|
|
||||||
export function hydrateStore(rawState) {
|
export function hydrateStore(rawState) {
|
||||||
const state = convertState(rawState);
|
return dispatch => {
|
||||||
|
const state = convertState(rawState);
|
||||||
|
|
||||||
return {
|
dispatch({
|
||||||
type: STORE_HYDRATE,
|
type: STORE_HYDRATE,
|
||||||
state,
|
state,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(hydrateCompose());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||||
word = str.slice(left, right + caretPosition);
|
word = str.slice(left, right + caretPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!word || word.trim().length < 3 || ['@', ':'].indexOf(word[0]) === -1) {
|
if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,6 +170,9 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
if (typeof suggestion === 'object') {
|
if (typeof suggestion === 'object') {
|
||||||
inner = <AutosuggestEmoji emoji={suggestion} />;
|
inner = <AutosuggestEmoji emoji={suggestion} />;
|
||||||
key = suggestion.id;
|
key = suggestion.id;
|
||||||
|
} else if (suggestion[0] === '#') {
|
||||||
|
inner = suggestion;
|
||||||
|
key = suggestion;
|
||||||
} else {
|
} else {
|
||||||
inner = <AutosuggestAccountContainer id={suggestion} />;
|
inner = <AutosuggestAccountContainer id={suggestion} />;
|
||||||
key = suggestion;
|
key = suggestion;
|
||||||
|
|
|
@ -16,6 +16,8 @@ import {
|
||||||
COMPOSE_SUGGESTIONS_CLEAR,
|
COMPOSE_SUGGESTIONS_CLEAR,
|
||||||
COMPOSE_SUGGESTIONS_READY,
|
COMPOSE_SUGGESTIONS_READY,
|
||||||
COMPOSE_SUGGESTION_SELECT,
|
COMPOSE_SUGGESTION_SELECT,
|
||||||
|
COMPOSE_SUGGESTION_TAGS_UPDATE,
|
||||||
|
COMPOSE_TAG_HISTORY_UPDATE,
|
||||||
COMPOSE_SENSITIVITY_CHANGE,
|
COMPOSE_SENSITIVITY_CHANGE,
|
||||||
COMPOSE_SPOILERNESS_CHANGE,
|
COMPOSE_SPOILERNESS_CHANGE,
|
||||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||||
|
@ -54,6 +56,7 @@ const initialState = ImmutableMap({
|
||||||
default_sensitive: false,
|
default_sensitive: false,
|
||||||
resetFileKey: Math.floor((Math.random() * 0x10000)),
|
resetFileKey: Math.floor((Math.random() * 0x10000)),
|
||||||
idempotencyKey: null,
|
idempotencyKey: null,
|
||||||
|
tagHistory: ImmutableList(),
|
||||||
});
|
});
|
||||||
|
|
||||||
function statusToTextMentions(state, status) {
|
function statusToTextMentions(state, status) {
|
||||||
|
@ -122,6 +125,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 insertEmoji = (state, position, emojiData) => {
|
||||||
const emoji = emojiData.native;
|
const emoji = emojiData.native;
|
||||||
|
|
||||||
|
@ -252,6 +267,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);
|
return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token);
|
||||||
case COMPOSE_SUGGESTION_SELECT:
|
case COMPOSE_SUGGESTION_SELECT:
|
||||||
return insertSuggestion(state, action.position, action.token, action.completion);
|
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:
|
case TIMELINE_DELETE:
|
||||||
if (action.id === state.get('in_reply_to')) {
|
if (action.id === state.get('in_reply_to')) {
|
||||||
return state.set('in_reply_to', null);
|
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 pushNotificationsSetting = new Settings('mastodon_push_notification_data');
|
||||||
|
export const tagHistory = new Settings('mastodon_tag_history');
|
||||||
|
|
Reference in New Issue