-
Notifications
You must be signed in to change notification settings - Fork 51
/
suggestions-actions.js
346 lines (306 loc) · 10.3 KB
/
suggestions-actions.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
import { getSuggestions } from '../api/suggestions'
import { waitForPhraseDetail } from '../utils/phrase-util'
import { debounce, isUndefined } from 'lodash'
import {
DIFF_SETTING_CHANGED,
SET_SUGGESTION_SEARCH_TYPE,
RESET_SUGGESTIONS_COPYING,
COPY_SUGGESTION,
TEXT_SUGGESTION_STARTED_COPYING,
TEXT_SUGGESTION_FINISHED_COPYING,
PHRASE_SUGGESTION_STARTED_COPYING,
PHRASE_SUGGESTION_FINISHED_COPYING,
TEXT_SUGGESTIONS_UPDATED,
SUGGESTION_SEARCH_TEXT_CHANGE,
PHRASE_SUGGESTIONS_UPDATED,
SUGGESTION_PANEL_HEIGHT_CHANGE,
SHOW_DETAIL_FOR_SUGGESTION_BY_INDEX
} from './suggestions-action-types'
import { updateSetting } from './settings-actions'
import { KEY_SUGGESTIONS_VISIBLE } from '../reducers/settings-reducer'
import { getSuggestionsPanelVisible } from '../reducers'
export function toggleSuggestions () {
return (dispatch, getState) => {
const visible = getSuggestionsPanelVisible(getState())
dispatch(updateSetting(KEY_SUGGESTIONS_VISIBLE, !visible))
}
}
/**
* Make phrase search visible or hidden.
*
* If the phrase search panel is shown, it will just hide the suggestions
* panel. If suggestions are hidden or showing text search suggestions, the
* suggestion panel will be visible and will show phrase suggestions.
*/
export function togglePhraseSuggestions () {
return (dispatch, getState) => {
const panelVisible = getSuggestionsPanelVisible(getState())
const phraseSearchVisible =
panelVisible && getState().suggestions.searchType === 'phrase'
dispatch(setSuggestionSearchType('phrase'))
if (phraseSearchVisible || !panelVisible) {
dispatch(toggleSuggestions())
}
}
}
export function diffSettingChanged () {
return { type: DIFF_SETTING_CHANGED }
}
export function clearSearch () {
return changeSearchText('')
}
/**
* Start a text search when the search text stops changing for a quarter-second.
*
* This must not be nested in the action creator function, otherwise each call
* uses a separate debounce copy and it doesn't actually work.
*/
const dispatchFindTextSuggestionsWhenInactive = debounce(
(dispatch, searchText) => {
dispatch(findTextSuggestions(searchText))
}, 250)
export function changeSearchText (searchText) {
return (dispatch, getState) => {
dispatch(suggestionSearchTextChange(searchText))
dispatchFindTextSuggestionsWhenInactive(dispatch, searchText)
}
}
export function setSuggestionSearchType (type) {
if (type !== 'phrase' && type !== 'text') {
console.error('invalid search type', type)
}
return { type: SET_SUGGESTION_SEARCH_TYPE, searchType: type }
}
export function toggleSearchType () {
return (dispatch, getState) => {
const wasTypeText = getState().suggestions.searchType === 'text'
if (!wasTypeText) {
dispatch(changeSearchText(''))
}
dispatch(setSuggestionSearchType(wasTypeText ? 'phrase' : 'text'))
}
}
export function resetSuggestionsCopying () {
return { type: RESET_SUGGESTIONS_COPYING }
}
export function copySuggestionN (index) {
// Decision: keep the logic in here to choose what to copy
// reason: reducers are not an easy place to follow complex logic,
// they should mainly handle merging data
return (dispatch, getState) => {
const { searchType } = getState().suggestions
const { selectedPhraseId } = getState().phrases
const panelVisible = getSuggestionsPanelVisible(getState())
const isTextSuggestions = panelVisible && searchType === 'text'
if (isTextSuggestions) {
dispatch(copyTextSuggestionN(index))
} else {
dispatch(copyPhraseSuggestionN(selectedPhraseId, index))
}
}
}
function copyTextSuggestionN (index) {
return (dispatch, getState) => {
const { suggestions } = getState().suggestions.textSearch
if (suggestions && index < suggestions.length) {
dispatch(textSuggestionStartedCopying(index))
dispatch(copySuggestion(suggestions[index]))
setTimeout(
() => dispatch(textSuggestionFinishedCopying(index)),
500)
}
}
}
function copyPhraseSuggestionN (phraseId, index) {
return (dispatch, getState) => {
const { searchByPhrase } = getState().suggestions
const { suggestions } = searchByPhrase[phraseId]
if (suggestions && index < suggestions.length) {
dispatch(phraseSuggestionStartedCopying(phraseId, index))
dispatch(copySuggestion(suggestions[index]))
setTimeout(
() => dispatch(phraseSuggestionFinishedCopying(phraseId, index)),
500)
}
}
}
function copySuggestion (suggestion) {
return { type: COPY_SUGGESTION, suggestion }
}
function textSuggestionStartedCopying (index) {
return { type: TEXT_SUGGESTION_STARTED_COPYING, index }
}
function textSuggestionFinishedCopying (index) {
return { type: TEXT_SUGGESTION_FINISHED_COPYING, index }
}
function phraseSuggestionStartedCopying (phraseId, index) {
return { type: PHRASE_SUGGESTION_STARTED_COPYING, phraseId, index }
}
function phraseSuggestionFinishedCopying (phraseId, index) {
return { type: PHRASE_SUGGESTION_FINISHED_COPYING, phraseId, index }
}
export function textSuggestionsUpdated (
{loading, searchStrings, suggestions, timestamp}) {
return {
type: TEXT_SUGGESTIONS_UPDATED,
loading,
searchStrings,
suggestions,
timestamp
}
}
export function suggestionSearchTextChange (text) {
return { type: SUGGESTION_SEARCH_TEXT_CHANGE, text: text }
}
// TODO may want to throttle as well to prevent generating too many concurrent
// requests on a slow connection (e.g. 5s latency = 20 requests)
export function findTextSuggestions (searchText) {
return (dispatch, getState) => {
// TODO also dispatch search timestamp to state
const timestamp = Date.now()
// TODO stop if this is a repeat of the current search
// TODO use cached search result if there is a recent one
// (alternating 'a' and backspace in textbox would only hit server
// once until the cached result for 'a' is old enough to be stale)
// empty search should immediately return no results and no search strings
if (!searchText) {
dispatch(textSuggestionsUpdated({
loading: false,
searchStrings: [],
suggestions: [],
timestamp
}))
return
}
const searchStrings = [searchText]
// dispatching this means that any earlier searches will not display their
// results (because their timestamp is older than the one for the loading
// search)
dispatch(textSuggestionsUpdated({
loading: true,
searchStrings,
suggestions: [],
timestamp
}))
const { context } = getState()
const sourceLocale = context.sourceLocale.localeId
const transLocale = context.lang
getSuggestions(sourceLocale, transLocale, searchStrings)
.then(suggestions => {
// only dispatch results if there is not a newer searches
// (but do dispatch when timestamp is the same, as it is an update of
// the current search progress)
const currentTimestamp = getState().suggestions.textSearch.timestamp
if (timestamp >= currentTimestamp) {
dispatch(textSuggestionsUpdated({
loading: false,
searchStrings,
suggestions,
timestamp
}))
}
// TODO trigger pending search if it exists
})
.catch(error => {
// TODO report error visible to user
// TODO set the text search to an error state, which can be displayed
console.error(error)
})
}
}
const TIMES_TO_POLL_FOR_PHRASE_DETAIL = 20
/**
* Trigger a phrase search using the detail for the given phrase id.
*
* When the detail is not available, this will retry every 0.5 seconds until
* the detail object is present, and will fail after 20 retries.
*
* This is needed mainly during document load because phrase selection happens
* before the detail is available.
*/
export function findPhraseSuggestionsById (phraseId) {
return (dispatch, getState) => {
if (isUndefined(phraseId)) {
return
}
waitForPhraseDetail(getState, phraseId, (phrase) => {
dispatch(findPhraseSuggestions(phrase))
}, TIMES_TO_POLL_FOR_PHRASE_DETAIL, () => {
console.error('No detail available for phrase search after 20 tries. ' +
'phraseId: ' + phraseId)
})
}
}
const PHRASE_SEARCH_STALE_AGE_MILLIS = 60000
export function findPhraseSuggestions (phrase) {
return (dispatch, getState) => {
const phraseId = phrase.id
const searchStrings = [...phrase.sources]
const timestamp = Date.now()
// if there are recent results, just leave them as-is and skip this search
const cachedSearch = getState().suggestions.searchByPhrase[phraseId]
if (cachedSearch) {
const age = timestamp - cachedSearch.timestamp
if (age < PHRASE_SEARCH_STALE_AGE_MILLIS) {
return
}
}
// set loading state, but only when there are no existing results
// (stale results are very likely accurate, so leave them as a placeholder)
if (!cachedSearch) {
dispatch(phraseSuggestionsUpdated({
phraseId,
loading: true,
searchStrings,
suggestions: [],
timestamp
}))
}
const { context } = getState()
const sourceLocale = context.sourceLocale.localeId
const transLocale = context.lang
getSuggestions(sourceLocale, transLocale, searchStrings)
.then(suggestions => {
dispatch(phraseSuggestionsUpdated({
phraseId,
loading: false,
searchStrings,
suggestions,
timestamp
}))
})
.catch(error => {
// TODO report error visible to user
console.error(error)
})
}
}
export function phraseSuggestionsUpdated (
{phraseId, loading, searchStrings, suggestions, timestamp}) {
return {
type: PHRASE_SUGGESTIONS_UPDATED,
phraseId,
loading,
searchStrings,
suggestions,
timestamp
}
}
export function saveSuggestionPanelHeight (percentageHeight) {
return {
type: SUGGESTION_PANEL_HEIGHT_CHANGE,
percentageHeight
}
}
/**
* Open or close the suggestion detail modal.
*
* @param indexOrUndefined undefined to hide the modal, index of suggestion in
* the current suggestion list to show detail for that suggestion in the modal
*/
export function showDetailForSuggestionByIndex (indexOrUndefined) {
return {
type: SHOW_DETAIL_FOR_SUGGESTION_BY_INDEX,
index: indexOrUndefined
}
}