mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-21 18:01:36 +03:00
7b443d4b63
no issue The `config` service has been a source of confusion when writing with modern Ember patterns because it's use of the deprecated `ProxyMixin` forced all property access/setting to go via `.get()` and `.set()` whereas the rest of the system has mostly (there are a few other uses of ProxyObjects remaining) eliminated the use of the non-native get/set methods. - removed use of `ProxyMixin` in the `config` service by grabbing the API response after fetching and using `Object.defineProperty()` to add native getters/setters that pass through to a tracked object holding the API response data. Ember's autotracking automatically works across the native getters/setters so we can then use the service as if it was any other native object - updated all code to use `config.{attrName}` directly for getting/setting instead of `.get()` and `.set()` - removed unnecessary async around `config.availableTimezones` which wasn't making any async calls
250 lines
7.0 KiB
JavaScript
250 lines
7.0 KiB
JavaScript
import Service, {inject as service} from '@ember/service';
|
|
import fetch from 'fetch';
|
|
import {TrackedArray} from 'tracked-built-ins';
|
|
import {action} from '@ember/object';
|
|
import {isEmpty} from '@ember/utils';
|
|
import {task, taskGroup, timeout} from 'ember-concurrency';
|
|
import {tracked} from '@glimmer/tracking';
|
|
|
|
const API_URL = 'https://tenor.googleapis.com';
|
|
const API_VERSION = 'v2';
|
|
const DEBOUNCE_MS = 600;
|
|
|
|
export default class TenorService extends Service {
|
|
@service config;
|
|
|
|
@tracked columnCount = 4;
|
|
@tracked columns = null;
|
|
@tracked error = null;
|
|
@tracked htmlError = null;
|
|
@tracked gifs = new TrackedArray([]);
|
|
@tracked searchTerm = '';
|
|
@tracked loadedType = '';
|
|
|
|
_columnHeights = [];
|
|
_nextPos = null;
|
|
|
|
get apiKey() {
|
|
return this.config.tenor.googleApiKey;
|
|
}
|
|
|
|
get contentfilter() {
|
|
return this.config.tenor.contentFilter || 'off';
|
|
}
|
|
|
|
get isLoading() {
|
|
return this.searchTask.isRunning || this.loadingTasks.isRunning;
|
|
}
|
|
|
|
constructor() {
|
|
super(...arguments);
|
|
this._resetColumns();
|
|
}
|
|
|
|
@action
|
|
updateSearch(term) {
|
|
if (term === this.searchTerm) {
|
|
return;
|
|
}
|
|
|
|
this.searchTerm = term;
|
|
this.reset();
|
|
|
|
if (term) {
|
|
return this.searchTask.perform(term);
|
|
} else {
|
|
return this.loadTrendingTask.perform();
|
|
}
|
|
}
|
|
|
|
@action
|
|
loadNextPage() {
|
|
// protect against scroll trigger firing when the gifs are reset
|
|
if (this.searchTask.isRunning) {
|
|
return;
|
|
}
|
|
|
|
if (isEmpty(this.gifs)) {
|
|
return this.loadTrendingTask.perform();
|
|
}
|
|
|
|
if (this._nextPos !== null) {
|
|
this.loadNextPageTask.perform();
|
|
}
|
|
}
|
|
|
|
@action
|
|
changeColumnCount(columnCount) {
|
|
this.columnCount = columnCount;
|
|
this._resetColumns();
|
|
}
|
|
|
|
@task({restartable: true})
|
|
*searchTask(term) {
|
|
yield timeout(DEBOUNCE_MS);
|
|
|
|
this.loadedType = 'search';
|
|
|
|
yield this._makeRequest(this.loadedType, {params: {
|
|
q: term,
|
|
media_filter: 'minimal'
|
|
}});
|
|
}
|
|
|
|
@taskGroup loadingTasks;
|
|
|
|
@task({group: 'loadingTasks'})
|
|
*loadTrendingTask() {
|
|
this.loadedType = 'featured';
|
|
|
|
yield this._makeRequest(this.loadedType, {params: {
|
|
q: 'excited',
|
|
media_filter: 'minimal'
|
|
}});
|
|
}
|
|
|
|
@task({group: 'loadingTasks'})
|
|
*loadNextPageTask() {
|
|
const params = {
|
|
pos: this._nextPos,
|
|
media_filter: 'minimal'
|
|
};
|
|
|
|
if (this.loadedType === 'search') {
|
|
params.q = this.searchTerm;
|
|
}
|
|
|
|
yield this._makeRequest(this.loadedType, {params});
|
|
}
|
|
|
|
@task({group: 'loadingTasks'})
|
|
*retryLastRequestTask() {
|
|
if (this._lastRequestArgs) {
|
|
yield this._makeRequest(...this._lastRequestArgs);
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
this.gifs = new TrackedArray([]);
|
|
this._nextPos = null;
|
|
this._resetColumns();
|
|
}
|
|
|
|
async _makeRequest(path, options) {
|
|
const versionedPath = `${API_VERSION}/${path}`.replace(/\/+/, '/');
|
|
const url = new URL(versionedPath, API_URL);
|
|
|
|
const params = new URLSearchParams(options.params);
|
|
params.set('key', this.apiKey);
|
|
params.set('client_key', 'ghost-editor');
|
|
params.set('contentfilter', this.contentfilter);
|
|
|
|
url.search = params.toString();
|
|
|
|
// store the url so it can be retried if needed
|
|
this._lastRequestArgs = arguments;
|
|
|
|
this.error = '';
|
|
|
|
return fetch(url)
|
|
.then(response => this._checkStatus(response))
|
|
.then(response => response.json())
|
|
.then(response => this._extractPagination(response))
|
|
.then(response => this._addGifsFromResponse(response))
|
|
.catch((e) => {
|
|
// if the error text isn't already set then we've get a connection error from `fetch`
|
|
if (!options.ignoreErrors && !this.error) {
|
|
this.error = 'Uh-oh! Trouble reaching the Tenor API, please check your connection';
|
|
}
|
|
|
|
if (this.error && this.error.startsWith('API key not valid')) {
|
|
// Added an html error field, so that we don't pass raw API errors from tenor to triple-braces in the frontend
|
|
this.htmlError = `This version of the Tenor API is no longer supported. Please update your API key by following our
|
|
<a href="https://ghost.org/docs/config/#tenor" target="_blank" rel="noopener noreferrer"> documentation here</a>.<br />`;
|
|
}
|
|
console.error(e); // eslint-disable-line
|
|
});
|
|
}
|
|
|
|
async _checkStatus(response) {
|
|
// successful request
|
|
if (response.status >= 200 && response.status < 300) {
|
|
return response;
|
|
}
|
|
|
|
let responseText;
|
|
|
|
if (response.headers.map['content-type'].startsWith('application/json')) {
|
|
responseText = await response.json().then(json => json.error.message || json.error);
|
|
} else if (response.headers.map['content-type'] === 'text/xml') {
|
|
responseText = await response.text();
|
|
}
|
|
|
|
this.error = responseText;
|
|
|
|
const error = new Error(responseText);
|
|
error.response = response;
|
|
throw error;
|
|
}
|
|
|
|
async _extractPagination(response) {
|
|
this._nextPos = response.next;
|
|
return response;
|
|
}
|
|
|
|
async _addGifsFromResponse(response) {
|
|
const gifs = response.results;
|
|
gifs.forEach(gif => this._addGif(gif));
|
|
|
|
return response;
|
|
}
|
|
|
|
_addGif(gif) {
|
|
// re-calculate ratio for later use
|
|
const [width, height] = gif.media_formats.tinygif.dims;
|
|
gif.ratio = height / width;
|
|
|
|
// add to general gifs list
|
|
this.gifs.push(gif);
|
|
|
|
// store index for use in templates and keyboard nav
|
|
gif.index = this.gifs.indexOf(gif);
|
|
|
|
// add to least populated column
|
|
this._addGifToColumns(gif);
|
|
}
|
|
|
|
_addGifToColumns(gif) {
|
|
const min = Math.min(...this._columnHeights);
|
|
const columnIndex = this._columnHeights.indexOf(min);
|
|
|
|
// use a fixed width when calculating height to compensate for different overall sizes
|
|
this._columnHeights[columnIndex] += 300 * gif.ratio;
|
|
this.columns[columnIndex].push(gif);
|
|
|
|
// store the column indexes on the gif for use in keyboard nav
|
|
gif.columnIndex = columnIndex;
|
|
gif.columnRowIndex = this.columns[columnIndex].length - 1;
|
|
}
|
|
|
|
_resetColumns() {
|
|
let columns = new TrackedArray([]);
|
|
let _columnHeights = [];
|
|
|
|
// pre-fill column arrays based on columnCount
|
|
for (let i = 0; i < this.columnCount; i += 1) {
|
|
columns[i] = new TrackedArray([]);
|
|
_columnHeights[i] = 0;
|
|
}
|
|
|
|
this.columns = columns;
|
|
this._columnHeights = _columnHeights;
|
|
|
|
if (!isEmpty(this.gifs)) {
|
|
this.gifs.forEach((gif) => {
|
|
this._addGifToColumns(gif);
|
|
});
|
|
}
|
|
}
|
|
}
|