Add GET /api/v2/search which returns rich tag objects, adjust web UI (#7661)
parent
90b64c0069
commit
8bb74e50be
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V2::SearchController < Api::V1::SearchController
|
||||||
|
def index
|
||||||
|
@search = Search.new(search)
|
||||||
|
render json: @search, serializer: REST::V2::SearchSerializer
|
||||||
|
end
|
||||||
|
end
|
|
@ -33,7 +33,7 @@ export function submitSearch() {
|
||||||
|
|
||||||
dispatch(fetchSearchRequest());
|
dispatch(fetchSearchRequest());
|
||||||
|
|
||||||
api(getState).get('/api/v1/search', {
|
api(getState).get('/api/v2/search', {
|
||||||
params: {
|
params: {
|
||||||
q: value,
|
q: value,
|
||||||
resolve: true,
|
resolve: true,
|
||||||
|
|
|
@ -16,6 +16,28 @@ const shortNumberFormat = number => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderHashtag = hashtag => (
|
||||||
|
<div className='trends__item' key={hashtag.get('name')}>
|
||||||
|
<div className='trends__item__name'>
|
||||||
|
<Link to={`/timelines/tag/${hashtag.get('name')}`}>
|
||||||
|
#<span>{hashtag.get('name')}</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='trends__item__current'>
|
||||||
|
{shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='trends__item__sparkline'>
|
||||||
|
<Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
|
||||||
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
|
</Sparklines>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default class SearchResults extends ImmutablePureComponent {
|
export default class SearchResults extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -44,27 +66,7 @@ export default class SearchResults extends ImmutablePureComponent {
|
||||||
<FormattedMessage id='trends.header' defaultMessage='Trending now' />
|
<FormattedMessage id='trends.header' defaultMessage='Trending now' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{trends && trends.map(hashtag => (
|
{trends && trends.map(hashtag => renderHashtag(hashtag))}
|
||||||
<div className='trends__item' key={hashtag.get('name')}>
|
|
||||||
<div className='trends__item__name'>
|
|
||||||
<Link to={`/timelines/tag/${hashtag.get('name')}`}>
|
|
||||||
#<span>{hashtag.get('name')}</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='trends__item__current'>
|
|
||||||
{shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='trends__item__sparkline'>
|
|
||||||
<Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
|
|
||||||
<SparklinesCurve style={{ fill: 'none' }} />
|
|
||||||
</Sparklines>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -74,7 +76,7 @@ export default class SearchResults extends ImmutablePureComponent {
|
||||||
count += results.get('accounts').size;
|
count += results.get('accounts').size;
|
||||||
accounts = (
|
accounts = (
|
||||||
<div className='search-results__section'>
|
<div className='search-results__section'>
|
||||||
<h5><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
|
<h5><i className='fa fa-fw fa-users' /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
|
||||||
|
|
||||||
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -85,7 +87,7 @@ export default class SearchResults extends ImmutablePureComponent {
|
||||||
count += results.get('statuses').size;
|
count += results.get('statuses').size;
|
||||||
statuses = (
|
statuses = (
|
||||||
<div className='search-results__section'>
|
<div className='search-results__section'>
|
||||||
<h5><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
|
<h5><i className='fa fa-fw fa-quote-right' /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
|
||||||
|
|
||||||
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -96,13 +98,9 @@ export default class SearchResults extends ImmutablePureComponent {
|
||||||
count += results.get('hashtags').size;
|
count += results.get('hashtags').size;
|
||||||
hashtags = (
|
hashtags = (
|
||||||
<div className='search-results__section'>
|
<div className='search-results__section'>
|
||||||
<h5><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
|
<h5><i className='fa fa-fw fa-hashtag' /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
|
||||||
|
|
||||||
{results.get('hashtags').map(hashtag => (
|
{results.get('hashtags').map(hashtag => renderHashtag(hashtag))}
|
||||||
<Link key={hashtag} className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}>
|
|
||||||
{hashtag}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
COMPOSE_REPLY,
|
COMPOSE_REPLY,
|
||||||
COMPOSE_DIRECT,
|
COMPOSE_DIRECT,
|
||||||
} from '../actions/compose';
|
} from '../actions/compose';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
value: '',
|
value: '',
|
||||||
|
@ -39,7 +39,7 @@ export default function search(state = initialState, action) {
|
||||||
return state.set('results', ImmutableMap({
|
return state.set('results', ImmutableMap({
|
||||||
accounts: ImmutableList(action.results.accounts.map(item => item.id)),
|
accounts: ImmutableList(action.results.accounts.map(item => item.id)),
|
||||||
statuses: ImmutableList(action.results.statuses.map(item => item.id)),
|
statuses: ImmutableList(action.results.statuses.map(item => item.id)),
|
||||||
hashtags: ImmutableList(action.results.hashtags),
|
hashtags: fromJS(action.results.hashtags),
|
||||||
})).set('submitted', true);
|
})).set('submitted', true);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -3284,6 +3284,15 @@ a.status-card {
|
||||||
}
|
}
|
||||||
|
|
||||||
.search__icon {
|
.search__icon {
|
||||||
|
&::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-focus-inner,
|
||||||
|
&:focus {
|
||||||
|
outline: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.fa {
|
.fa {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
|
@ -3333,7 +3342,6 @@ a.status-card {
|
||||||
.search-results__header {
|
.search-results__header {
|
||||||
color: $dark-text-color;
|
color: $dark-text-color;
|
||||||
background: lighten($ui-base-color, 2%);
|
background: lighten($ui-base-color, 2%);
|
||||||
border-bottom: 1px solid darken($ui-base-color, 4%);
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
@ -3346,33 +3354,21 @@ a.status-card {
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-results__section {
|
.search-results__section {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 5px;
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
position: relative;
|
background: darken($ui-base-color, 4%);
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
cursor: default;
|
||||||
|
display: flex;
|
||||||
|
padding: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
color: $dark-text-color;
|
||||||
|
|
||||||
&::before {
|
.fa {
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 50%;
|
|
||||||
width: 100%;
|
|
||||||
height: 0;
|
|
||||||
border-top: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: $ui-base-color;
|
margin-right: 5px;
|
||||||
color: $darker-text-color;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 10px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
cursor: default;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::V2::SearchSerializer < ActiveModel::Serializer
|
||||||
|
has_many :accounts, serializer: REST::AccountSerializer
|
||||||
|
has_many :statuses, serializer: REST::StatusSerializer
|
||||||
|
has_many :hashtags, serializer: REST::TagSerializer
|
||||||
|
end
|
|
@ -315,6 +315,10 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
namespace :v2 do
|
||||||
|
get '/search', to: 'search#index', as: :search
|
||||||
|
end
|
||||||
|
|
||||||
namespace :web do
|
namespace :web do
|
||||||
resource :settings, only: [:update]
|
resource :settings, only: [:update]
|
||||||
resource :embed, only: [:create]
|
resource :embed, only: [:create]
|
||||||
|
|
Reference in New Issue