-
+
{results.get('accounts').map(accountId =>
)}
diff --git a/app/javascript/mastodon/features/compose/containers/search_container.js b/app/javascript/mastodon/features/compose/containers/search_container.js
index 392bd0f566..3ee55fae59 100644
--- a/app/javascript/mastodon/features/compose/containers/search_container.js
+++ b/app/javascript/mastodon/features/compose/containers/search_container.js
@@ -4,12 +4,16 @@ import {
clearSearch,
submitSearch,
showSearch,
-} from '../../../actions/search';
+ openURL,
+ clickSearchResult,
+ forgetSearchResult,
+} from 'mastodon/actions/search';
import Search from '../components/search';
const mapStateToProps = state => ({
value: state.getIn(['search', 'value']),
submitted: state.getIn(['search', 'submitted']),
+ recent: state.getIn(['search', 'recent']),
});
const mapDispatchToProps = dispatch => ({
@@ -22,14 +26,26 @@ const mapDispatchToProps = dispatch => ({
dispatch(clearSearch());
},
- onSubmit () {
- dispatch(submitSearch());
+ onSubmit (type) {
+ dispatch(submitSearch(type));
},
onShow () {
dispatch(showSearch());
},
+ onOpenURL (routerHistory) {
+ dispatch(openURL(routerHistory));
+ },
+
+ onClickSearchResult (q, type) {
+ dispatch(clickSearchResult(q, type));
+ },
+
+ onForgetSearchResult (q) {
+ dispatch(forgetSearchResult(q));
+ },
+
});
export default connect(mapStateToProps, mapDispatchToProps)(Search);
diff --git a/app/javascript/mastodon/features/compose/containers/warning_container.jsx b/app/javascript/mastodon/features/compose/containers/warning_container.jsx
index 3c6ed483d0..e99f5dacd9 100644
--- a/app/javascript/mastodon/features/compose/containers/warning_container.jsx
+++ b/app/javascript/mastodon/features/compose/containers/warning_container.jsx
@@ -3,36 +3,12 @@ import { connect } from 'react-redux';
import Warning from '../components/warning';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
-import { me } from '../../../initial_state';
-
-const buildHashtagRE = () => {
- try {
- const HASHTAG_SEPARATORS = '_\\u00b7\\u200c';
- const ALPHA = '\\p{L}\\p{M}';
- const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
- return new RegExp(
- '(?:^|[^\\/\\)\\w])#((' +
- '[' + WORD + '_]' +
- '[' + WORD + HASHTAG_SEPARATORS + ']*' +
- '[' + ALPHA + HASHTAG_SEPARATORS + ']' +
- '[' + WORD + HASHTAG_SEPARATORS +']*' +
- '[' + WORD + '_]' +
- ')|(' +
- '[' + WORD + '_]*' +
- '[' + ALPHA + ']' +
- '[' + WORD + '_]*' +
- '))', 'iu',
- );
- } catch {
- return /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;
- }
-};
-
-const APPROX_HASHTAG_RE = buildHashtagRE();
+import { me } from 'mastodon/initial_state';
+import { HASHTAG_PATTERN_REGEX } from 'mastodon/utils/hashtags';
const mapStateToProps = state => ({
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
- hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])),
+ hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])),
directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
});
diff --git a/app/javascript/mastodon/features/explore/results.jsx b/app/javascript/mastodon/features/explore/results.jsx
index 27132f1328..9725cf35cd 100644
--- a/app/javascript/mastodon/features/explore/results.jsx
+++ b/app/javascript/mastodon/features/explore/results.jsx
@@ -105,7 +105,7 @@ class Results extends React.PureComponent {
-
+
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 3bdba3b1d5..3d59fa01d7 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -530,7 +530,7 @@
"search_popout.tips.status": "post",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.user": "user",
- "search_results.accounts": "People",
+ "search_results.accounts": "Profiles",
"search_results.all": "All",
"search_results.hashtags": "Hashtags",
"search_results.nothing_found": "Could not find anything for these search terms",
diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js
index d3e71da9d9..e545f430cc 100644
--- a/app/javascript/mastodon/reducers/search.js
+++ b/app/javascript/mastodon/reducers/search.js
@@ -6,13 +6,15 @@ import {
SEARCH_FETCH_SUCCESS,
SEARCH_SHOW,
SEARCH_EXPAND_SUCCESS,
+ SEARCH_RESULT_CLICK,
+ SEARCH_RESULT_FORGET,
} from '../actions/search';
import {
COMPOSE_MENTION,
COMPOSE_REPLY,
COMPOSE_DIRECT,
} from '../actions/compose';
-import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
const initialState = ImmutableMap({
value: '',
@@ -21,6 +23,7 @@ const initialState = ImmutableMap({
results: ImmutableMap(),
isLoading: false,
searchTerm: '',
+ recent: ImmutableOrderedSet(),
});
export default function search(state = initialState, action) {
@@ -61,6 +64,10 @@ export default function search(state = initialState, action) {
case SEARCH_EXPAND_SUCCESS:
const results = action.searchType === 'hashtags' ? fromJS(action.results.hashtags) : action.results[action.searchType].map(item => item.id);
return state.updateIn(['results', action.searchType], list => list.concat(results));
+ case SEARCH_RESULT_CLICK:
+ return state.update('recent', set => set.add(fromJS(action.result)));
+ case SEARCH_RESULT_FORGET:
+ return state.update('recent', set => set.filterNot(result => result.get('q') === action.q));
default:
return state;
}
diff --git a/app/javascript/mastodon/utils/hashtags.js b/app/javascript/mastodon/utils/hashtags.js
new file mode 100644
index 0000000000..358ce37f54
--- /dev/null
+++ b/app/javascript/mastodon/utils/hashtags.js
@@ -0,0 +1,47 @@
+const HASHTAG_SEPARATORS = '_\\u00b7\\u200c';
+const ALPHA = '\\p{L}\\p{M}';
+const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
+
+const buildHashtagPatternRegex = () => {
+ try {
+ return new RegExp(
+ '(?:^|[^\\/\\)\\w])#((' +
+ '[' + WORD + '_]' +
+ '[' + WORD + HASHTAG_SEPARATORS + ']*' +
+ '[' + ALPHA + HASHTAG_SEPARATORS + ']' +
+ '[' + WORD + HASHTAG_SEPARATORS +']*' +
+ '[' + WORD + '_]' +
+ ')|(' +
+ '[' + WORD + '_]*' +
+ '[' + ALPHA + ']' +
+ '[' + WORD + '_]*' +
+ '))', 'iu',
+ );
+ } catch {
+ return /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;
+ }
+};
+
+const buildHashtagRegex = () => {
+ try {
+ return new RegExp(
+ '^((' +
+ '[' + WORD + '_]' +
+ '[' + WORD + HASHTAG_SEPARATORS + ']*' +
+ '[' + ALPHA + HASHTAG_SEPARATORS + ']' +
+ '[' + WORD + HASHTAG_SEPARATORS +']*' +
+ '[' + WORD + '_]' +
+ ')|(' +
+ '[' + WORD + '_]*' +
+ '[' + ALPHA + ']' +
+ '[' + WORD + '_]*' +
+ '))$', 'iu',
+ );
+ } catch {
+ return /^(\w*[a-zA-Z·]\w*)$/i;
+ }
+};
+
+export const HASHTAG_PATTERN_REGEX = buildHashtagPatternRegex();
+
+export const HASHTAG_REGEX = buildHashtagRegex();
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 32dcd59b66..6681aa75c4 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -4816,6 +4816,86 @@ a.status-card.compact:hover {
.search {
margin-bottom: 10px;
position: relative;
+
+ &__popout {
+ box-sizing: border-box;
+ display: none;
+ position: absolute;
+ inset-inline-start: 0;
+ margin-top: -2px;
+ width: 100%;
+ background: $ui-base-color;
+ border-radius: 0 0 4px 4px;
+ box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
+ z-index: 2;
+ font-size: 13px;
+ padding: 15px 5px;
+
+ h4 {
+ text-transform: uppercase;
+ color: $dark-text-color;
+ font-weight: 500;
+ padding: 0 10px;
+ margin-bottom: 10px;
+ }
+
+ &__menu {
+ &__message {
+ color: $dark-text-color;
+ padding: 0 10px;
+ }
+
+ &__item {
+ display: block;
+ box-sizing: border-box;
+ width: 100%;
+ border: 0;
+ font: inherit;
+ background: transparent;
+ color: $darker-text-color;
+ padding: 10px;
+ cursor: pointer;
+ border-radius: 4px;
+ text-align: start;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+
+ &--flex {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .icon-button {
+ transition: none;
+ }
+
+ &:hover,
+ &:focus,
+ &:active,
+ &.selected {
+ background: $ui-highlight-color;
+ color: $primary-text-color;
+
+ .icon-button {
+ color: $primary-text-color;
+ }
+ }
+
+ mark {
+ background: transparent;
+ font-weight: 700;
+ color: $primary-text-color;
+ }
+ }
+ }
+ }
+
+ &.active {
+ .search__popout {
+ display: block;
+ }
+ }
}
.search__input {
@@ -6695,10 +6775,6 @@ a.status-card.compact:hover {
border-radius: 0;
}
-.search-popout {
- @include search-popout;
-}
-
noscript {
text-align: center;
@@ -7985,6 +8061,10 @@ noscript {
padding: 10px;
}
+ .search__popout {
+ border: 1px solid lighten($ui-base-color, 8%);
+ }
+
.search .fa {
top: 10px;
inset-inline-end: 10px;