import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
+import ImmutablePropTypes from 'react-immutable-proptypes';
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>
<DisplayName account={account} />
</div>
);
+AutosuggestAccount.propTypes = {
+ account: ImmutablePropTypes.map.isRequired
+};
+
export default AutosuggestAccount;
--- /dev/null
+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;
import ImmutablePropTypes from 'react-immutable-proptypes';
import Autosuggest from 'react-autosuggest';
import AutosuggestAccountContainer from '../containers/autosuggest_account_container';
+import AutosuggestStatusContainer from '../containers/autosuggest_status_container';
import { debounce } from 'react-decoration';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
const renderSuggestion = suggestion => {
if (suggestion.type === 'account') {
return <AutosuggestAccountContainer id={suggestion.id} />;
+ } else if (suggestion.type === 'hashtag') {
+ return <span>#{suggestion.id}</span>;
} else {
- return <span>#{suggestion.id}</span>
+ return <AutosuggestStatusContainer id={suggestion.id} />;
}
};
onSuggestionSelected (_, { suggestion }) {
if (suggestion.type === 'account') {
this.context.router.push(`/accounts/${suggestion.id}`);
- } else {
+ } else if(suggestion.type === 'hashtag') {
this.context.router.push(`/timelines/tag/${suggestion.id}`);
+ } else {
+ this.context.router.push(`/statuses/${suggestion.id}`);
}
},
--- /dev/null
+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);
case REBLOGS_FETCH_SUCCESS:
case FAVOURITES_FETCH_SUCCESS:
case COMPOSE_SUGGESTIONS_READY:
- case SEARCH_SUGGESTIONS_READY:
case FOLLOW_REQUESTS_FETCH_SUCCESS:
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
case BLOCKS_FETCH_SUCCESS:
return normalizeAccounts(state, action.accounts);
case NOTIFICATIONS_REFRESH_SUCCESS:
case NOTIFICATIONS_EXPAND_SUCCESS:
+ case SEARCH_SUGGESTIONS_READY:
return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses);
case TIMELINE_REFRESH_SUCCESS:
case TIMELINE_EXPAND_SUCCESS:
value: `#${item}`
}));
- if (value.indexOf('@') === -1 && value.indexOf(' ') === -1 && hashtags.indexOf(value) === -1) {
+ if (value.indexOf('@') === -1 && value.indexOf(' ') === -1 && !value.startsWith('http://') && !value.startsWith('https://') && hashtags.indexOf(value) === -1) {
hashtagItems.unshift({
type: 'hashtag',
id: value,
});
}
+ if (hashtagItems.length > 0) {
+ newSuggestions.push({
+ title: 'hashtag',
+ items: hashtagItems
+ });
+ }
+ }
+
+ if (statuses.length > 0) {
newSuggestions.push({
- title: 'hashtag',
- items: hashtagItems
+ title: 'status',
+ items: statuses.map(item => ({
+ type: 'status',
+ id: item.id,
+ value: item.id
+ }))
});
}
}
}
}
+
+.autosuggest-status {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ strong {
+ font-weight: 500;
+ }
+}
end
def search_for(terms, limit = 10)
+ terms = Arel.sql(connection.quote(terms.gsub(/['?\\:]/, ' ')))
textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))'
- query = 'to_tsquery(\'simple\', \'\'\' \' || ? || \' \'\'\' || \':*\')'
+ query = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')'
sql = <<SQL
SELECT
LIMIT ?
SQL
- Account.find_by_sql([sql, terms, terms, limit])
+ Account.find_by_sql([sql, limit])
end
def advanced_search_for(terms, account, limit = 10)
+ terms = Arel.sql(connection.quote(terms.gsub(/['?\\:]/, ' ')))
textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))'
- query = 'to_tsquery(\'simple\', \'\'\' \' || ? || \' \'\'\' || \':*\')'
+ query = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')'
sql = <<SQL
SELECT
LIMIT ?
SQL
- Account.find_by_sql([sql, terms, account.id, account.id, terms, limit])
+ Account.find_by_sql([sql, account.id, account.id, limit])
end
def following_map(target_account_ids, account_id)
class << self
def search_for(terms, limit = 5)
+ terms = Arel.sql(connection.quote(terms.gsub(/['?\\:]/, ' ')))
textsearch = 'to_tsvector(\'simple\', tags.name)'
- query = 'to_tsquery(\'simple\', \'\'\' \' || ? || \' \'\'\' || \':*\')'
+ query = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')'
sql = <<SQL
SELECT
LIMIT ?
SQL
- Tag.find_by_sql([sql, terms, terms, limit])
+ Tag.find_by_sql([sql, limit])
end
end
end
# frozen_string_literal: true
class FetchRemoteAccountService < BaseService
- def call(url)
- atom_url, body = FetchAtomService.new.call(url)
+ def call(url, prefetched_body = nil)
+ if prefetched_body.nil?
+ atom_url, body = FetchAtomService.new.call(url)
+ else
+ atom_url = url
+ body = prefetched_body
+ end
return nil if atom_url.nil?
process_atom(atom_url, body)
xml.encoding = 'utf-8'
if xml.root.name == 'feed'
- FetchRemoteAccountService.new.call(atom_url)
+ FetchRemoteAccountService.new.call(atom_url, body)
elsif xml.root.name == 'entry'
- FetchRemoteStatusService.new.call(atom_url)
+ FetchRemoteStatusService.new.call(atom_url, body)
end
end
end
# frozen_string_literal: true
class FetchRemoteStatusService < BaseService
- def call(url)
- atom_url, body = FetchAtomService.new.call(url)
+ def call(url, prefetched_body = nil)
+ if prefetched_body.nil?
+ atom_url, body = FetchAtomService.new.call(url)
+ else
+ atom_url = url
+ body = prefetched_body
+ end
return nil if atom_url.nil?
process_atom(atom_url, body)
--- /dev/null
+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
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170322143850) do
+ActiveRecord::Schema.define(version: 20170322162804) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
t.string "name", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index "to_tsvector('simple'::regconfig, (name)::text)", name: "hashtag_search_index", using: :gin
t.index ["name"], name: "index_tags_on_name", unique: true, using: :btree
end