diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/components/actions/search.jsx | 8 | ||||
-rw-r--r-- | app/assets/javascripts/components/reducers/search.jsx | 58 | ||||
-rw-r--r-- | app/assets/javascripts/components/reducers/statuses.jsx | 2 | ||||
-rw-r--r-- | app/controllers/api/v1/accounts_controller.rb | 2 | ||||
-rw-r--r-- | app/controllers/api/v1/search_controller.rb | 9 | ||||
-rw-r--r-- | app/models/tag.rb | 19 | ||||
-rw-r--r-- | app/services/account_search_service.rb | 26 | ||||
-rw-r--r-- | app/services/fetch_remote_resource_service.rb | 18 | ||||
-rw-r--r-- | app/services/search_service.rb | 23 | ||||
-rw-r--r-- | app/views/api/v1/search/index.rabl | 13 |
10 files changed, 136 insertions, 42 deletions
diff --git a/app/assets/javascripts/components/actions/search.jsx b/app/assets/javascripts/components/actions/search.jsx index ceb0e4a0c..e4af716ee 100644 --- a/app/assets/javascripts/components/actions/search.jsx +++ b/app/assets/javascripts/components/actions/search.jsx @@ -18,11 +18,13 @@ export function clearSearchSuggestions() { }; }; -export function readySearchSuggestions(value, accounts) { +export function readySearchSuggestions(value, { accounts, hashtags, statuses }) { return { type: SEARCH_SUGGESTIONS_READY, value, - accounts + accounts, + hashtags, + statuses }; }; @@ -32,7 +34,7 @@ export function fetchSearchSuggestions(value) { return; } - api(getState).get('/api/v1/accounts/search', { + api(getState).get('/api/v1/search', { params: { q: value, resolve: true, diff --git a/app/assets/javascripts/components/reducers/search.jsx b/app/assets/javascripts/components/reducers/search.jsx index d835ef268..1767be2c6 100644 --- a/app/assets/javascripts/components/reducers/search.jsx +++ b/app/assets/javascripts/components/reducers/search.jsx @@ -11,28 +11,38 @@ const initialState = Immutable.Map({ suggestions: [] }); -const normalizeSuggestions = (state, value, accounts) => { - let newSuggestions = [ - { +const normalizeSuggestions = (state, value, accounts, hashtags, statuses) => { + let newSuggestions = []; + + if (accounts.length > 0) { + newSuggestions.push({ title: 'account', items: accounts.map(item => ({ type: 'account', id: item.id, value: item.acct })) + }); + } + + if (value.indexOf('@') === -1 && value.indexOf(' ') === -1 || hashtags.length > 0) { + let hashtagItems = hashtags.map(item => ({ + type: 'hashtag', + id: item, + value: `#${item}` + })); + + if (value.indexOf('@') === -1 && value.indexOf(' ') === -1 && hashtags.indexOf(value) === -1) { + hashtagItems.unshift({ + type: 'hashtag', + id: value, + value: `#${value}` + }); } - ]; - if (value.indexOf('@') === -1 && value.indexOf(' ') === -1) { newSuggestions.push({ title: 'hashtag', - items: [ - { - type: 'hashtag', - id: value, - value: `#${value}` - } - ] + items: hashtagItems }); } @@ -44,17 +54,17 @@ const normalizeSuggestions = (state, value, accounts) => { export default function search(state = initialState, action) { switch(action.type) { - case SEARCH_CHANGE: - return state.set('value', action.value); - case SEARCH_SUGGESTIONS_READY: - return normalizeSuggestions(state, action.value, action.accounts); - case SEARCH_RESET: - return state.withMutations(map => { - map.set('suggestions', []); - map.set('value', ''); - map.set('loaded_value', ''); - }); - default: - return state; + case SEARCH_CHANGE: + return state.set('value', action.value); + case SEARCH_SUGGESTIONS_READY: + return normalizeSuggestions(state, action.value, action.accounts, action.hashtags, action.statuses); + case SEARCH_RESET: + return state.withMutations(map => { + map.set('suggestions', []); + map.set('value', ''); + map.set('loaded_value', ''); + }); + default: + return state; } }; diff --git a/app/assets/javascripts/components/reducers/statuses.jsx b/app/assets/javascripts/components/reducers/statuses.jsx index ce791eab6..1669b8c65 100644 --- a/app/assets/javascripts/components/reducers/statuses.jsx +++ b/app/assets/javascripts/components/reducers/statuses.jsx @@ -32,6 +32,7 @@ import { FAVOURITED_STATUSES_FETCH_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS } from '../actions/favourites'; +import { SEARCH_SUGGESTIONS_READY } from '../actions/search'; import Immutable from 'immutable'; const normalizeStatus = (state, status) => { @@ -108,6 +109,7 @@ export default function statuses(state = initialState, action) { case NOTIFICATIONS_EXPAND_SUCCESS: case FAVOURITED_STATUSES_FETCH_SUCCESS: case FAVOURITED_STATUSES_EXPAND_SUCCESS: + case SEARCH_SUGGESTIONS_READY: return normalizeStatuses(state, action.statuses); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.references); diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 51c948955..f07450eb0 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -115,7 +115,7 @@ class Api::V1::AccountsController < ApiController end def search - @accounts = SearchService.new.call(params[:q], limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:resolve] == 'true', current_account) + @accounts = AccountSearchService.new.call(params[:q], limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:resolve] == 'true', current_account) set_account_counters_maps(@accounts) unless @accounts.nil? diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb new file mode 100644 index 000000000..6b1292458 --- /dev/null +++ b/app/controllers/api/v1/search_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Api::V1::SearchController < ApiController + respond_to :json + + def index + @search = OpenStruct.new(SearchService.new.call(params[:q], 5, params[:resolve] == 'true', current_account)) + end +end diff --git a/app/models/tag.rb b/app/models/tag.rb index 0d2fe43b8..e2ad8e4db 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -10,4 +10,23 @@ class Tag < ApplicationRecord def to_param name end + + class << self + def search_for(terms, limit = 5) + textsearch = 'to_tsvector(\'simple\', tags.name)' + query = 'to_tsquery(\'simple\', \'\'\' \' || ? || \' \'\'\' || \':*\')' + + sql = <<SQL + SELECT + tags.*, + ts_rank_cd(#{textsearch}, #{query}) AS rank + FROM tags + WHERE #{query} @@ #{textsearch} + ORDER BY rank DESC + LIMIT ? +SQL + + Tag.find_by_sql([sql, terms, terms, limit]) + end + end end diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb new file mode 100644 index 000000000..f55439dcb --- /dev/null +++ b/app/services/account_search_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class AccountSearchService < BaseService + def call(query, limit, resolve = false, account = nil) + return [] if query.blank? || query.start_with?('#') + + username, domain = query.gsub(/\A@/, '').split('@') + domain = nil if TagManager.instance.local_domain?(domain) + + if domain.nil? + exact_match = Account.find_local(username) + results = account.nil? ? Account.search_for(username, limit) : Account.advanced_search_for(username, account, limit) + else + exact_match = Account.find_remote(username, domain) + results = account.nil? ? Account.search_for("#{username} #{domain}", limit) : Account.advanced_search_for("#{username} #{domain}", account, limit) + end + + results = [exact_match] + results.reject { |a| a.id == exact_match.id } if exact_match + + if resolve && !exact_match && !domain.nil? + results = [FollowRemoteAccountService.new.call("#{username}@#{domain}")] + end + + results + end +end diff --git a/app/services/fetch_remote_resource_service.rb b/app/services/fetch_remote_resource_service.rb new file mode 100644 index 000000000..80aa74365 --- /dev/null +++ b/app/services/fetch_remote_resource_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class FetchRemoteResourceService < BaseService + def call(url) + atom_url, body = FetchAtomService.new.call(url) + + return nil if atom_url.nil? + + xml = Nokogiri::XML(body) + xml.encoding = 'utf-8' + + if xml.root.name == 'feed' + FetchRemoteAccountService.new.call(atom_url) + elsif xml.root.name == 'entry' + FetchRemoteStatusService.new.call(atom_url) + end + end +end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 19fc16973..159c03713 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -2,23 +2,18 @@ class SearchService < BaseService def call(query, limit, resolve = false, account = nil) - return if query.blank? || query.start_with?('#') + return if query.blank? - username, domain = query.gsub(/\A@/, '').split('@') - domain = nil if TagManager.instance.local_domain?(domain) + results = { accounts: [], hashtags: [], statuses: [] } - if domain.nil? - exact_match = Account.find_local(username) - results = account.nil? ? Account.search_for(username, limit) : Account.advanced_search_for(username, account, limit) - else - exact_match = Account.find_remote(username, domain) - results = account.nil? ? Account.search_for("#{username} #{domain}", limit) : Account.advanced_search_for("#{username} #{domain}", account, limit) - end + if query =~ /\Ahttps?:\/\// + resource = FetchRemoteResourceService.new.call(query) - results = [exact_match] + results.reject { |a| a.id == exact_match.id } if exact_match - - if resolve && !exact_match && !domain.nil? - results = [FollowRemoteAccountService.new.call("#{username}@#{domain}")] + results[:accounts] << resource if resource.is_a?(Account) + results[:statuses] << resource if resource.is_a?(Status) + else + results[:accounts] = AccountSearchService.new.call(query, limit, resolve, account) + results[:hashtags] = Tag.search_for(query.gsub(/\A#/, ''), limit) unless query.start_with?('@') end results diff --git a/app/views/api/v1/search/index.rabl b/app/views/api/v1/search/index.rabl new file mode 100644 index 000000000..8d1640f2d --- /dev/null +++ b/app/views/api/v1/search/index.rabl @@ -0,0 +1,13 @@ +object @search + +child :accounts, object_root: false do + extends 'api/v1/accounts/show' +end + +node(:hashtags) do |search| + search.hashtags.map(&:name) +end + +child :statuses, object_root: false do + extends 'api/v1/statuses/show' +end |