diff options
34 files changed, 248 insertions, 139 deletions
diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb index 1bb19a09d..b6877fb3c 100644 --- a/app/controllers/api/v1/custom_emojis_controller.rb +++ b/app/controllers/api/v1/custom_emojis_controller.rb @@ -7,7 +7,7 @@ class Api::V1::CustomEmojisController < Api::BaseController def index render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do - ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer) + ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false).includes(:category), each_serializer: REST::CustomEmojiSerializer) end end end diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js index b2d24e10b..9ce77b24b 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -48,7 +48,7 @@ export function submitSearch() { dispatch(importFetchedStatuses(response.data.statuses)); } - dispatch(fetchSearchSuccess(response.data)); + dispatch(fetchSearchSuccess(response.data, value)); dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); }).catch(error => { dispatch(fetchSearchFail(error)); @@ -62,12 +62,11 @@ export function fetchSearchRequest() { }; }; -export function fetchSearchSuccess(results) { +export function fetchSearchSuccess(results, searchTerm) { return { type: SEARCH_FETCH_SUCCESS, results, - accounts: results.accounts, - statuses: results.statuses, + searchTerm, }; }; diff --git a/app/javascript/flavours/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js index 1d96933ea..b25555d0f 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.js +++ b/app/javascript/flavours/glitch/features/compose/components/search.js @@ -36,7 +36,7 @@ class SearchPopout extends React.PureComponent { <div style={{ ...style, position: 'absolute', width: 285, zIndex: 2 }}> <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> {({ opacity, scaleX, scaleY }) => ( - <div className='drawer--search--popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> + <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> <h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4> <ul> @@ -128,14 +128,14 @@ class Search extends React.PureComponent { render () { const { intl, value, submitted } = this.props; const { expanded } = this.state; - const active = value.length > 0 || submitted; - const computedClass = classNames('drawer--search', { active }); + const hasValue = value.length > 0 || submitted; return ( - <div className={computedClass}> + <div className='search'> <label> <span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span> <input + className='search__input' type='text' placeholder={intl.formatMessage(messages.placeholder)} value={value || ''} @@ -145,17 +145,19 @@ class Search extends React.PureComponent { onBlur={this.handleBlur} /> </label> + <div aria-label={intl.formatMessage(messages.placeholder)} - className='icon' + className='search__icon' onClick={this.handleClear} role='button' tabIndex='0' > - <Icon icon='search' /> - <Icon icon='times-circle' /> + <Icon icon='search' className={hasValue ? '' : 'active'} /> + <Icon icon='times-circle' className={hasValue ? 'active' : ''} /> </div> - <Overlay show={expanded && !active} placement='bottom' target={this}> + + <Overlay show={expanded && !hasValue} placement='bottom' target={this}> <SearchPopout /> </Overlay> </div> diff --git a/app/javascript/flavours/glitch/features/compose/components/search_results.js b/app/javascript/flavours/glitch/features/compose/components/search_results.js index 69df8cdc9..dd99f3430 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search_results.js +++ b/app/javascript/flavours/glitch/features/compose/components/search_results.js @@ -7,6 +7,7 @@ import StatusContainer from 'flavours/glitch/containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Hashtag from 'flavours/glitch/components/hashtag'; import Icon from 'flavours/glitch/components/icon'; +import { searchEnabled } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, @@ -20,6 +21,7 @@ class SearchResults extends ImmutablePureComponent { suggestions: ImmutablePropTypes.list.isRequired, fetchSuggestions: PropTypes.func.isRequired, dismissSuggestion: PropTypes.func.isRequired, + searchTerm: PropTypes.string, intl: PropTypes.object.isRequired, }; @@ -27,8 +29,8 @@ class SearchResults extends ImmutablePureComponent { this.props.fetchSuggestions(); } - render() { - const { intl, results, suggestions, dismissSuggestion } = this.props; + render () { + const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props; if (results.isEmpty() && !suggestions.isEmpty()) { return ( @@ -51,6 +53,16 @@ class SearchResults extends ImmutablePureComponent { </div> </div> ); + } else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) { + statuses = ( + <section> + <h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> + + <div className='search-results__info'> + <FormattedMessage id='search_results.statuses_fts_disabled' defaultMessage='Searching toots by their content is not enabled on this Mastodon server.' /> + </div> + </section> + ); } let accounts, statuses, hashtags; diff --git a/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js b/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js index f9637861a..e4d5f3420 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js @@ -5,6 +5,7 @@ import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestion const mapStateToProps = state => ({ results: state.getIn(['search', 'results']), suggestions: state.getIn(['suggestions', 'items']), + searchTerm: state.getIn(['search', 'searchTerm']), }); const mapDispatchToProps = dispatch => ({ diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js index a78117971..9821502d3 100644 --- a/app/javascript/flavours/glitch/features/emoji_picker/index.js +++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js @@ -11,7 +11,7 @@ import Overlay from 'react-overlays/lib/Overlay'; import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import detectPassiveEvents from 'detect-passive-events'; -import { buildCustomEmojis } from 'flavours/glitch/util/emoji'; +import { buildCustomEmojis, categoriesFromEmojis } from 'flavours/glitch/util/emoji'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -110,19 +110,6 @@ let EmojiPicker, Emoji; // load asynchronously const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`; const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; -const categoriesSort = [ - 'recent', - 'custom', - 'people', - 'nature', - 'foods', - 'activity', - 'places', - 'objects', - 'symbols', - 'flags', -]; - class ModifierPickerMenu extends React.PureComponent { static propTypes = { @@ -320,8 +307,23 @@ class EmojiPickerMenu extends React.PureComponent { } const title = intl.formatMessage(messages.emoji); + const { modifierOpen } = this.state; + const categoriesSort = [ + 'recent', + 'people', + 'nature', + 'foods', + 'activity', + 'places', + 'objects', + 'symbols', + 'flags', + ]; + + categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort()); + return ( <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}> <EmojiPicker diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index 3a188ca87..30097f064 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -112,6 +112,11 @@ export default class ColumnsArea extends ImmutablePureComponent { // React-router does this for us, but too late, feeling laggy. document.querySelector(currentLinkSelector).classList.remove('active'); document.querySelector(nextLinkSelector).classList.add('active'); + + if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') { + this.context.router.history.push(getLink(this.pendingIndex)); + this.pendingIndex = null; + } } handleAnimationEnd = () => { @@ -162,7 +167,6 @@ export default class ColumnsArea extends ImmutablePureComponent { const { shouldAnimate } = this.state; const columnIndex = getIndex(this.context.router.history.location.pathname); - this.pendingIndex = null; if (singleColumn) { const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><i className='fa fa-pencil' /></Link>; diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 787488db4..e072c22ec 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -317,7 +317,7 @@ export default class UI extends React.Component { handleHotkeySearch = e => { e.preventDefault(); - const element = this.node.querySelector('.drawer--search input'); + const element = this.node.querySelector('.search__input'); if (element) { element.focus(); diff --git a/app/javascript/flavours/glitch/reducers/search.js b/app/javascript/flavours/glitch/reducers/search.js index 9a525bf47..1c32a5b9f 100644 --- a/app/javascript/flavours/glitch/reducers/search.js +++ b/app/javascript/flavours/glitch/reducers/search.js @@ -16,6 +16,7 @@ const initialState = ImmutableMap({ submitted: false, hidden: false, results: ImmutableMap(), + searchTerm: '', }); export default function search(state = initialState, action) { @@ -40,7 +41,7 @@ export default function search(state = initialState, action) { accounts: ImmutableList(action.results.accounts.map(item => item.id)), statuses: ImmutableList(action.results.statuses.map(item => item.id)), hashtags: fromJS(action.results.hashtags), - })).set('submitted', true); + })).set('submitted', true).set('searchTerm', action.searchTerm); default: return state; } diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss index 0e50482f6..0994a9b43 100644 --- a/app/javascript/flavours/glitch/styles/components/drawer.scss +++ b/app/javascript/flavours/glitch/styles/components/drawer.scss @@ -73,90 +73,16 @@ } } -.drawer--search { +.search { position: relative; margin-bottom: 10px; flex: none; @include limited-single-column('screen and (max-width: 360px)') { margin-bottom: 0 } @include single-column('screen and (max-width: 630px)') { font-size: 16px } - - input { - display: block; - box-sizing: border-box; - margin: 0; - border: none; - padding: 15px 30px 15px 15px; - width: 100%; - outline: 0; - color: $darker-text-color; - background: $ui-base-color; - font-size: 14px; - font-family: inherit; - line-height: 16px; - - &:focus { - outline: 0; - background: lighten($ui-base-color, 4%); - } - } - - & > .icon { - display: block; - position: absolute; - top: 10px; - right: 10px; - width: 18px; - height: 18px; - color: $secondary-text-color; - font-size: 18px; - line-height: 18px; - z-index: 2; - - .fa { - display: inline-block; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - opacity: 0; - cursor: default; - pointer-events: none; - transition: all 100ms linear; - transition-property: color, transform, opacity; - } - - .fa-search { - opacity: 0.3; - transform: rotate(0deg); - } - - .fa-times-circle { - transform: rotate(-90deg); - cursor: pointer; - - &:hover { color: $primary-text-color } - } - } - - &.active { - & > .icon { - .fa-search { - opacity: 0; - transform: rotate(90deg); - } - - .fa-times-circle { - opacity: 0.3; - pointer-events: auto; - transform: rotate(0deg); - } - } - } } -.drawer--search--popout { +.search-popout { @include search-popout(); } diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss index 3ef141133..117da362f 100644 --- a/app/javascript/flavours/glitch/styles/components/search.scss +++ b/app/javascript/flavours/glitch/styles/components/search.scss @@ -3,13 +3,25 @@ } .search__input { + @include search-input(); + display: block; - padding: 10px; + padding: 15px; padding-right: 30px; - @include search-input(); + line-height: 18px; + font-size: 16px; } .search__icon { + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, + &:focus { + outline: 0 !important; + } + .fa { position: absolute; top: 16px; @@ -18,7 +30,7 @@ display: inline-block; opacity: 0; transition: all 100ms linear; - transition-property: transform, opacity; + transition-property: color, transform, opacity; font-size: 18px; width: 18px; height: 18px; @@ -33,17 +45,18 @@ } .fa-search { - transform: rotate(90deg); + transform: rotate(0deg); &.active { - pointer-events: none; - transform: rotate(0deg); + pointer-events: auto; + opacity: 0.3; } } .fa-times-circle { top: 17px; transform: rotate(0deg); + color: $action-button-color; cursor: pointer; &.active { @@ -51,7 +64,7 @@ } &:hover { - color: $primary-text-color; + color: lighten($action-button-color, 7%); } } } @@ -65,6 +78,11 @@ font-weight: 500; } +.search-results__info { + padding: 10px; + color: $secondary-text-color; +} + .trends { &__header { color: $dark-text-color; diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss index ca962abd2..e0f3d62a7 100644 --- a/app/javascript/flavours/glitch/styles/components/single_column.scss +++ b/app/javascript/flavours/glitch/styles/components/single_column.scss @@ -6,7 +6,7 @@ height: calc(100% - 10px); overflow-y: hidden; - .drawer--search input { + .search__input { line-height: 18px; font-size: 16px; padding: 15px; diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js index 82a1ef89c..e6fcaf0dc 100644 --- a/app/javascript/flavours/glitch/util/emoji/index.js +++ b/app/javascript/flavours/glitch/util/emoji/index.js @@ -93,8 +93,11 @@ export const buildCustomEmojis = (customEmojis) => { keywords: [name], imageUrl: url, custom: true, + customCategory: emoji.get('category'), }); }); return emojis; }; + +export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set()); diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index 7c06670eb..0974fdd15 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -48,7 +48,7 @@ export function submitSearch() { dispatch(importFetchedStatuses(response.data.statuses)); } - dispatch(fetchSearchSuccess(response.data)); + dispatch(fetchSearchSuccess(response.data, value)); dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); }).catch(error => { dispatch(fetchSearchFail(error)); @@ -62,10 +62,11 @@ export function fetchSearchRequest() { }; }; -export function fetchSearchSuccess(results) { +export function fetchSearchSuccess(results, searchTerm) { return { type: SEARCH_FETCH_SUCCESS, results, + searchTerm, }; }; diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index c1429c756..e57c3c20c 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -6,7 +6,7 @@ import Overlay from 'react-overlays/lib/Overlay'; import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import detectPassiveEvents from 'detect-passive-events'; -import { buildCustomEmojis } from '../../emoji/emoji'; +import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -31,19 +31,6 @@ let EmojiPicker, Emoji; // load asynchronously const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`; const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; -const categoriesSort = [ - 'recent', - 'custom', - 'people', - 'nature', - 'foods', - 'activity', - 'places', - 'objects', - 'symbols', - 'flags', -]; - class ModifierPickerMenu extends React.PureComponent { static propTypes = { @@ -241,8 +228,23 @@ class EmojiPickerMenu extends React.PureComponent { } const title = intl.formatMessage(messages.emoji); + const { modifierOpen } = this.state; + const categoriesSort = [ + 'recent', + 'people', + 'nature', + 'foods', + 'activity', + 'places', + 'objects', + 'symbols', + 'flags', + ]; + + categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort()); + return ( <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}> <EmojiPicker diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index e0966f52c..2f338dd24 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.js @@ -7,6 +7,7 @@ import StatusContainer from '../../../containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Hashtag from '../../../components/hashtag'; import Icon from 'mastodon/components/icon'; +import { searchEnabled } from '../../../initial_state'; const messages = defineMessages({ dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, @@ -20,6 +21,7 @@ class SearchResults extends ImmutablePureComponent { suggestions: ImmutablePropTypes.list.isRequired, fetchSuggestions: PropTypes.func.isRequired, dismissSuggestion: PropTypes.func.isRequired, + searchTerm: PropTypes.string, intl: PropTypes.object.isRequired, }; @@ -28,7 +30,7 @@ class SearchResults extends ImmutablePureComponent { } render () { - const { intl, results, suggestions, dismissSuggestion } = this.props; + const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props; if (results.isEmpty() && !suggestions.isEmpty()) { return ( @@ -76,6 +78,16 @@ class SearchResults extends ImmutablePureComponent { {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} </div> ); + } else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) { + statuses = ( + <div className='search-results__section'> + <h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> + + <div className='search-results__info'> + <FormattedMessage id='search_results.statuses_fts_disabled' defaultMessage='Searching toots by their content is not enabled on this Mastodon server.' /> + </div> + </div> + ); } if (results.get('hashtags') && results.get('hashtags').size > 0) { diff --git a/app/javascript/mastodon/features/compose/containers/search_results_container.js b/app/javascript/mastodon/features/compose/containers/search_results_container.js index f9637861a..e4d5f3420 100644 --- a/app/javascript/mastodon/features/compose/containers/search_results_container.js +++ b/app/javascript/mastodon/features/compose/containers/search_results_container.js @@ -5,6 +5,7 @@ import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestion const mapStateToProps = state => ({ results: state.getIn(['search', 'results']), suggestions: state.getIn(['suggestions', 'items']), + searchTerm: state.getIn(['search', 'searchTerm']), }); const mapDispatchToProps = dispatch => ({ diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index 988cea253..01b5a6664 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -92,8 +92,11 @@ export const buildCustomEmojis = (customEmojis) => { keywords: [name], imageUrl: url, custom: true, + customCategory: emoji.get('category'), }); }); return emojis; }; + +export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set()); diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 756db3c61..042e44e43 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -110,6 +110,11 @@ class ColumnsArea extends ImmutablePureComponent { // React-router does this for us, but too late, feeling laggy. document.querySelector(currentLinkSelector).classList.remove('active'); document.querySelector(nextLinkSelector).classList.add('active'); + + if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') { + this.context.router.history.push(getLink(this.pendingIndex)); + this.pendingIndex = null; + } } handleAnimationEnd = () => { @@ -160,7 +165,6 @@ class ColumnsArea extends ImmutablePureComponent { const { shouldAnimate } = this.state; const columnIndex = getIndex(this.context.router.history.location.pathname); - this.pendingIndex = null; if (singleColumn) { const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js index 4758defb1..77b7f588c 100644 --- a/app/javascript/mastodon/reducers/search.js +++ b/app/javascript/mastodon/reducers/search.js @@ -16,6 +16,7 @@ const initialState = ImmutableMap({ submitted: false, hidden: false, results: ImmutableMap(), + searchTerm: '', }); export default function search(state = initialState, action) { @@ -40,7 +41,7 @@ export default function search(state = initialState, action) { accounts: ImmutableList(action.results.accounts.map(item => item.id)), statuses: ImmutableList(action.results.statuses.map(item => item.id)), hashtags: fromJS(action.results.hashtags), - })).set('submitted', true); + })).set('submitted', true).set('searchTerm', action.searchTerm); default: return state; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index dc750dbfe..1063d1836 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3996,6 +3996,11 @@ a.status-card.compact:hover { } } +.search-results__info { + padding: 10px; + color: $secondary-text-color; +} + .modal-root { position: relative; transition: opacity 0.3s linear; diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index e73cd9bd2..514cf4845 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -16,6 +16,7 @@ # uri :string # image_remote_url :string # visible_in_picker :boolean default(TRUE), not null +# category_id :bigint(8) # class CustomEmoji < ApplicationRecord @@ -27,6 +28,7 @@ class CustomEmoji < ApplicationRecord :(#{SHORTCODE_RE_FRAGMENT}): (?=[^[:alnum:]:]|$)/x + belongs_to :category, class_name: 'CustomEmojiCategory', optional: true has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce -strip' } } diff --git a/app/models/custom_emoji_category.rb b/app/models/custom_emoji_category.rb new file mode 100644 index 000000000..7d8c0ee2d --- /dev/null +++ b/app/models/custom_emoji_category.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: custom_emoji_categories +# +# id :bigint(8) not null, primary key +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# + +class CustomEmojiCategory < ApplicationRecord + has_many :emojis, class_name: 'CustomEmoji', foreign_key: 'category_id', inverse_of: :category +end diff --git a/app/serializers/activitypub/update_poll_serializer.rb b/app/serializers/activitypub/update_poll_serializer.rb index b894f309f..1d47b9764 100644 --- a/app/serializers/activitypub/update_poll_serializer.rb +++ b/app/serializers/activitypub/update_poll_serializer.rb @@ -14,7 +14,7 @@ class ActivityPub::UpdatePollSerializer < ActivityPub::Serializer end def actor - ActivityPub::TagManager.instance.uri_for(object) + ActivityPub::TagManager.instance.uri_for(object.account) end def to diff --git a/app/serializers/rest/custom_emoji_serializer.rb b/app/serializers/rest/custom_emoji_serializer.rb index 65686a866..aff58e4d9 100644 --- a/app/serializers/rest/custom_emoji_serializer.rb +++ b/app/serializers/rest/custom_emoji_serializer.rb @@ -5,6 +5,8 @@ class REST::CustomEmojiSerializer < ActiveModel::Serializer attributes :shortcode, :url, :static_url, :visible_in_picker + attribute :category, if: :category_loaded? + def url full_asset_url(object.image.url) end @@ -12,4 +14,12 @@ class REST::CustomEmojiSerializer < ActiveModel::Serializer def static_url full_asset_url(object.image.url(:static)) end + + def category + object.category.name + end + + def category_loaded? + object.association(:category).loaded? && object.category.present? + end end diff --git a/db/migrate/20190627222225_create_custom_emoji_categories.rb b/db/migrate/20190627222225_create_custom_emoji_categories.rb new file mode 100644 index 000000000..4713793e6 --- /dev/null +++ b/db/migrate/20190627222225_create_custom_emoji_categories.rb @@ -0,0 +1,9 @@ +class CreateCustomEmojiCategories < ActiveRecord::Migration[5.2] + def change + create_table :custom_emoji_categories do |t| + t.string :name, index: { unique: true } + + t.timestamps + end + end +end diff --git a/db/migrate/20190627222826_add_category_id_to_custom_emojis.rb b/db/migrate/20190627222826_add_category_id_to_custom_emojis.rb new file mode 100644 index 000000000..873b4d05f --- /dev/null +++ b/db/migrate/20190627222826_add_category_id_to_custom_emojis.rb @@ -0,0 +1,5 @@ +class AddCategoryIdToCustomEmojis < ActiveRecord::Migration[5.2] + def change + add_column :custom_emojis, :category_id, :bigint + end +end diff --git a/db/schema.rb b/db/schema.rb index aa02be55f..2e7af9b78 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_05_29_143559) do +ActiveRecord::Schema.define(version: 2019_06_27_222826) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -218,6 +218,13 @@ ActiveRecord::Schema.define(version: 2019_05_29_143559) do t.index ["uri"], name: "index_conversations_on_uri", unique: true end + create_table "custom_emoji_categories", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["name"], name: "index_custom_emoji_categories_on_name", unique: true + end + create_table "custom_emojis", force: :cascade do |t| t.string "shortcode", default: "", null: false t.string "domain" @@ -231,6 +238,7 @@ ActiveRecord::Schema.define(version: 2019_05_29_143559) do t.string "uri" t.string "image_remote_url" t.boolean "visible_in_picker", default: true, null: false + t.bigint "category_id" t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true end diff --git a/lib/mastodon/emoji_cli.rb b/lib/mastodon/emoji_cli.rb index 97a822e45..beac1b1fd 100644 --- a/lib/mastodon/emoji_cli.rb +++ b/lib/mastodon/emoji_cli.rb @@ -15,6 +15,7 @@ module Mastodon option :suffix option :overwrite, type: :boolean option :unlisted, type: :boolean + option :category desc 'import PATH', 'Import emoji from a TAR GZIP archive at PATH' long_desc <<-LONG_DESC Imports custom emoji from a TAR GZIP archive specified by PATH. @@ -22,6 +23,9 @@ module Mastodon Existing emoji will be skipped unless the --overwrite option is provided, in which case they will be overwritten. + You can specifiy a --category under which the emojis will be + grouped together. + With the --prefix option, a prefix can be added to all generated shortcodes. Likewise, the --suffix option controls the suffix of all shortcodes. @@ -33,6 +37,7 @@ module Mastodon imported = 0 skipped = 0 failed = 0 + category = options[:category] ? CustomEmojiCategory.find_or_create_by(name: options[:category]) : nil Gem::Package::TarReader.new(Zlib::GzipReader.open(path)) do |tar| tar.each do |entry| @@ -50,6 +55,7 @@ module Mastodon custom_emoji.image = StringIO.new(entry.read) custom_emoji.image_file_name = File.basename(entry.full_name) custom_emoji.visible_in_picker = !options[:unlisted] + custom_emoji.category = category if custom_emoji.save imported += 1 diff --git a/spec/fabricators/custom_emoji_category_fabricator.rb b/spec/fabricators/custom_emoji_category_fabricator.rb new file mode 100644 index 000000000..f593b95ed --- /dev/null +++ b/spec/fabricators/custom_emoji_category_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:custom_emoji_category) do + name "MyString" +end diff --git a/spec/models/custom_emoji_category_spec.rb b/spec/models/custom_emoji_category_spec.rb new file mode 100644 index 000000000..160033f4d --- /dev/null +++ b/spec/models/custom_emoji_category_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe CustomEmojiCategory, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/serializers/activitypub/update_poll_spec.rb b/spec/serializers/activitypub/update_poll_spec.rb new file mode 100644 index 000000000..f9e035eab --- /dev/null +++ b/spec/serializers/activitypub/update_poll_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ActivityPub::UpdatePollSerializer do + let(:account) { Fabricate(:account) } + let(:poll) { Fabricate(:poll, account: account) } + let!(:status) { Fabricate(:status, account: account, poll: poll) } + + before(:each) do + @serialization = ActiveModelSerializers::SerializableResource.new(status, serializer: ActivityPub::UpdatePollSerializer, adapter: ActivityPub::Adapter) + end + + subject { JSON.parse(@serialization.to_json) } + + it 'has a Update type' do + expect(subject['type']).to eql('Update') + end + + it 'has an object with Question type' do + expect(subject['object']['type']).to eql('Question') + end + + it 'has the correct actor URI set' do + expect(subject['actor']).to eql(ActivityPub::TagManager.instance.uri_for(account)) + end +end diff --git a/spec/workers/activitypub/distribute_poll_update_worker_spec.rb b/spec/workers/activitypub/distribute_poll_update_worker_spec.rb new file mode 100644 index 000000000..7eb6119fd --- /dev/null +++ b/spec/workers/activitypub/distribute_poll_update_worker_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +describe ActivityPub::DistributePollUpdateWorker do + subject { described_class.new } + + let(:account) { Fabricate(:account) } + let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com') } + let(:poll) { Fabricate(:poll, account: account) } + let!(:status) { Fabricate(:status, account: account, poll: poll) } + + describe '#perform' do + before do + allow(ActivityPub::DeliveryWorker).to receive(:push_bulk) + follower.follow!(account) + end + + it 'delivers to followers' do + subject.perform(status.id) + expect(ActivityPub::DeliveryWorker).to have_received(:push_bulk).with(['http://example.com']) + end + end +end diff --git a/yarn.lock b/yarn.lock index 776473f77..a7d98fe3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3388,8 +3388,8 @@ elliptic@^6.0.0: minimalistic-crypto-utils "^1.0.0" emoji-mart@Gargron/emoji-mart#build: - version "2.6.2" - resolved "https://codeload.github.com/Gargron/emoji-mart/tar.gz/ff00dc470b5b2d9f145a6d6e977a54de5df2b4c9" + version "2.6.3" + resolved "https://codeload.github.com/Gargron/emoji-mart/tar.gz/934f314fd8322276765066e8a2a6be5bac61b1cf" emoji-regex@^7.0.1, emoji-regex@^7.0.2: version "7.0.3" |