@ -1,11 +1,16 @@ | |||||
import Avatar from '../../../components/avatar'; | import Avatar from '../../../components/avatar'; | ||||
import DisplayName from '../../../components/display_name'; | import DisplayName from '../../../components/display_name'; | ||||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||||
const AutosuggestAccount = ({ account }) => ( | const AutosuggestAccount = ({ account }) => ( | ||||
<div style={{ overflow: 'hidden' }}> | |||||
<div style={{ overflow: 'hidden' }} className='autosuggest-account'> | |||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div> | <div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div> | ||||
<DisplayName account={account} /> | <DisplayName account={account} /> | ||||
</div> | </div> | ||||
); | ); | ||||
AutosuggestAccount.propTypes = { | |||||
account: ImmutablePropTypes.map.isRequired | |||||
}; | |||||
export default AutosuggestAccount; | export default AutosuggestAccount; |
@ -0,0 +1,15 @@ | |||||
import { FormattedMessage } from 'react-intl'; | |||||
import DisplayName from '../../../components/display_name'; | |||||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||||
const AutosuggestStatus = ({ status }) => ( | |||||
<div style={{ overflow: 'hidden' }} className='autosuggest-status'> | |||||
<FormattedMessage id='search.status_by' defaultMessage='Status by {name}' values={{ name: <strong>@{status.getIn(['account', 'acct'])}</strong> }} /> | |||||
</div> | |||||
); | |||||
AutosuggestStatus.propTypes = { | |||||
status: ImmutablePropTypes.map.isRequired | |||||
}; | |||||
export default AutosuggestStatus; |
@ -0,0 +1,15 @@ | |||||
import { connect } from 'react-redux'; | |||||
import AutosuggestStatus from '../components/autosuggest_status'; | |||||
import { makeGetStatus } from '../../../selectors'; | |||||
const makeMapStateToProps = () => { | |||||
const getStatus = makeGetStatus(); | |||||
const mapStateToProps = (state, { id }) => ({ | |||||
status: getStatus(state, id) | |||||
}); | |||||
return mapStateToProps; | |||||
}; | |||||
export default connect(makeMapStateToProps)(AutosuggestStatus); |
@ -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 |
@ -0,0 +1,39 @@ | |||||
# frozen_string_literal: true | |||||
class StatusesController < ApplicationController | |||||
layout 'public' | |||||
before_action :set_account | |||||
before_action :set_status | |||||
before_action :set_link_headers | |||||
before_action :check_account_suspension | |||||
def show | |||||
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : [] | |||||
@descendants = cache_collection(@status.descendants(current_account), Status) | |||||
render 'stream_entries/show' | |||||
end | |||||
private | |||||
def set_account | |||||
@account = Account.find_local!(params[:account_username]) | |||||
end | |||||
def set_link_headers | |||||
response.headers['Link'] = LinkHeader.new([[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]]]) | |||||
end | |||||
def set_status | |||||
@status = @account.statuses.find(params[:id]) | |||||
@stream_entry = @status.stream_entry | |||||
@type = @stream_entry.activity_type.downcase | |||||
raise ActiveRecord::RecordNotFound unless @status.permitted?(current_account) | |||||
end | |||||
def check_account_suspension | |||||
gone if @account.suspended? | |||||
end | |||||
end |
@ -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 |
@ -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, body) | |||||
elsif xml.root.name == 'entry' | |||||
FetchRemoteStatusService.new.call(atom_url, body) | |||||
end | |||||
end | |||||
end |
@ -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 |
@ -0,0 +1,9 @@ | |||||
class AddSearchIndexToTags < ActiveRecord::Migration[5.0] | |||||
def up | |||||
execute 'CREATE INDEX hashtag_search_index ON tags USING gin(to_tsvector(\'simple\', tags.name));' | |||||
end | |||||
def down | |||||
remove_index :tags, name: :hashtag_search_index | |||||
end | |||||
end |