Progress on group account search

Added group member search, group removed account, album add styles
This commit is contained in:
mgabdev 2020-12-20 19:28:32 -05:00
parent 67eb9d5890
commit 1a8ecc672c
20 changed files with 225 additions and 94 deletions

View File

@ -34,6 +34,10 @@ class Api::V1::Groups::RemovedAccountsController < Api::BaseController
render_empty_success
end
def search
# : todo :
end
private
def set_group

View File

@ -113,6 +113,16 @@ class Api::V1::GroupsController < Api::BaseController
render_empty_success
end
def member_search
@accounts = Group.search_for_members(@group, params[:q], DEFAULT_ACCOUNTS_LIMIT)
render json: @accounts, each_serializer: REST::AccountSerializer
end
def removed_accounts_search
@accounts = Group.search_for_removed_accounts(@group, params[:q], DEFAULT_ACCOUNTS_LIMIT)
render json: @accounts, each_serializer: REST::AccountSerializer
end
private
def set_group

View File

@ -2,6 +2,7 @@ import {
Map as ImmutableMap,
List as ImmutableList,
} from 'immutable'
import debounce from 'lodash.debounce'
import api, { getLinks } from '../api'
import { me } from '../initial_state'
import { importFetchedAccounts } from './importer'
@ -33,6 +34,8 @@ export const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST'
export const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS'
export const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL'
//
export const GROUP_MEMBERS_FETCH_REQUEST = 'GROUP_MEMBERS_FETCH_REQUEST'
export const GROUP_MEMBERS_FETCH_SUCCESS = 'GROUP_MEMBERS_FETCH_SUCCESS'
export const GROUP_MEMBERS_FETCH_FAIL = 'GROUP_MEMBERS_FETCH_FAIL'
@ -41,6 +44,11 @@ export const GROUP_MEMBERS_EXPAND_REQUEST = 'GROUP_MEMBERS_EXPAND_REQUEST'
export const GROUP_MEMBERS_EXPAND_SUCCESS = 'GROUP_MEMBERS_EXPAND_SUCCESS'
export const GROUP_MEMBERS_EXPAND_FAIL = 'GROUP_MEMBERS_EXPAND_FAIL'
export const GROUP_MEMBERS_SEARCH_SUCCESS = 'GROUP_MEMBERS_SEARCH_SUCCESS'
export const CLEAR_GROUP_MEMBERS_SEARCH = 'CLEAR_GROUP_MEMBERS_SEARCH'
//
export const GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST = 'GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST'
export const GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS'
export const GROUP_REMOVED_ACCOUNTS_FETCH_FAIL = 'GROUP_REMOVED_ACCOUNTS_FETCH_FAIL'
@ -49,6 +57,11 @@ export const GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST = 'GROUP_REMOVED_ACCOUNTS_EXP
export const GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS'
export const GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL = 'GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL'
export const GROUP_REMOVED_ACCOUNTS_SEARCH_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_SEARCH_SUCCESS'
export const CLEAR_GROUP_REMOVED_ACCOUNTS_SEARCH = 'CLEAR_GROUP_REMOVED_ACCOUNTS_SEARCH'
//
export const GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST'
export const GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS'
export const GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL = 'GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL'
@ -57,6 +70,8 @@ export const GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_CRE
export const GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS'
export const GROUP_REMOVED_ACCOUNTS_CREATE_FAIL = 'GROUP_REMOVED_ACCOUNTS_CREATE_FAIL'
//
export const GROUP_JOIN_REQUESTS_FETCH_REQUEST = 'GROUP_JOIN_REQUESTS_FETCH_REQUEST'
export const GROUP_JOIN_REQUESTS_FETCH_SUCCESS = 'GROUP_JOIN_REQUESTS_FETCH_SUCCESS'
export const GROUP_JOIN_REQUESTS_FETCH_FAIL = 'GROUP_JOIN_REQUESTS_FETCH_FAIL'
@ -457,7 +472,7 @@ const expandMembersRequest = (groupId) => ({
type: GROUP_MEMBERS_EXPAND_REQUEST,
groupId,
})
``
const expandMembersSuccess = (groupId, accounts, next) => ({
type: GROUP_MEMBERS_EXPAND_SUCCESS,
groupId,
@ -472,6 +487,43 @@ const expandMembersFail = (groupId, error) => ({
error,
})
/**
*
*/
export const fetchGroupMembersAdminSearch = (groupId, query) => (dispatch, getState) => {
if (!groupId || !query) return
debouncedFetchGroupMembersAdminSearch(groupId, query, dispatch, getState)
}
export const debouncedFetchGroupMembersAdminSearch = debounce((groupId, query, dispatch, getState) => {
if (!groupId || !query) return
api(getState).get(`/api/v1/groups/${groupId}/member_search`, {
params: {
q: query,
resolve: false,
limit: 4,
},
}).then((response) => {
dispatch(importFetchedAccounts(response.data))
dispatch(fetchGroupMembersAdminSearchSuccess(response.data))
}).catch((error) => {
//
})
}, 650, { leading: true })
const fetchGroupMembersAdminSearchSuccess = (accounts) => ({
type: GROUP_MEMBERS_SEARCH_SUCCESS,
accounts,
})
/**
*
*/
export const clearGroupMembersAdminSearch = () => (dispatch) => {
dispatch({ type: CLEAR_GROUP_MEMBERS_SEARCH })
}
/**
* @description Fetch removed accounts for the given groupId and imports paginated
* accounts and sets in user_lists reducer.
@ -557,6 +609,43 @@ const expandRemovedAccountsFail = (groupId, error) => ({
error,
})
/**
*
*/
export const fetchGroupRemovedAccountsAdminSearch = (groupId, query) => (dispatch, getState) => {
if (!groupId || !query) return
debouncedFetchGroupRemovedAccountsAdminSearch(groupId, query, dispatch, getState)
}
export const debouncedFetchGroupRemovedAccountsAdminSearch = debounce((groupId, query, dispatch, getState) => {
if (!groupId || !query) return
api(getState).get(`/api/v1/groups/${groupId}/removed_accounts_search`, {
params: {
q: query,
resolve: false,
limit: 4,
},
}).then((response) => {
dispatch(importFetchedAccounts(response.data))
dispatch(fetchGroupRemovedAccountsAdminSearchSuccess(response.data))
}).catch((error) => {
//
})
}, 650, { leading: true })
const fetchGroupRemovedAccountsAdminSearchSuccess = (accounts) => ({
type: GROUP_REMOVED_ACCOUNTS_SEARCH_SUCCESS,
accounts,
})
/**
*
*/
export const clearGroupRemovedAccountsAdminSearch = () => (dispatch) => {
dispatch({ type: CLEAR_GROUP_REMOVED_ACCOUNTS_SEARCH })
}
/**
* @description Remove a "removed account" from a group with the given groupId and accountId.
* @param {String} groupId

View File

@ -64,7 +64,7 @@ class Account extends ImmutablePureComponent {
)
}
const actionButton = (onActionClick && actionIcon) ? (
const actionButton = (onActionClick && (actionIcon || actionTitle)) ? (
<Button
onClick={this.handleAction}
isOutline={true}

View File

@ -1,6 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import { NavLink } from 'react-router-dom'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { CX } from '../constants'
@ -15,29 +14,45 @@ class Album extends React.PureComponent {
//
}
handleOnOpenAlbumCreation = () => {
}
render() {
const { album, isDummy } = this.props
const {
album,
isAddable,
isDummy,
} = this.props
const title = isAddable ? 'New album' : 'Album title'
const subtitle = isAddable ? '' : '10 Items'
const to = isAddable ? undefined : `/photos`
return (
<div className={[_s.d, _s.minW162PX, _s.px5, _s.flex1].join(' ')}>
{
!isDummy &&
<NavLink
className={[_s.d, _s.noUnderline].join(' ')}
to='/'
<Button
noClasses
className={[_s.d, _s.noUnderline, _s.noOutline, _s.bgTransparent].join(' ')}
to={to}
onClick={isAddable ? this.handleOnOpenAlbumCreation : undefined}
>
<div className={[_s.d, _s.w100PC, _s.mt5, _s.mb10].join(' ')}>
<div className={[_s.d, _s.w100PC, _s.pt100PC].join(' ')}>
<div className={[_s.d, _s.posAbs, _s.top0, _s.w100PC, _s.right0, _s.bottom0, _s.left0].join(' ')}>
<div className={[_s.d, _s.w100PC, _s.h100PC, _s.radiusSmall, _s.bgTertiary, _s.border1PX, _s.borderColorSecondary].join(' ')} />
<div className={[_s.d, _s.w100PC, _s.h100PC, _s.aiCenter, _s.jcCenter, _s.radiusSmall, _s.bgTertiary, _s.border1PX, _s.borderColorSecondary].join(' ')}>
{ isAddable && <Icon id='add' size='20px' /> }
</div>
</div>
</div>
</div>
<div className={[_s.d, _s.w100PC, _s.pt7, _s.mb15].join(' ')}>
<Text weight='bold'>Profile Photos</Text>
<Text color='secondary' size='small' className={_s.mt5}>1 Item</Text>
<Text weight='bold'>{title}</Text>
{ !isAddable && <Text color='secondary' size='small' className={_s.mt5}>{subtitle}</Text> }
</div>
</NavLink>
</Button>
}
</div>
)

View File

@ -95,6 +95,8 @@ class MediaItem extends ImmutablePureComponent {
const statusUrl = `/${account.getIn(['acct'])}/posts/${status.get('id')}`;
// : todo : fix dimensions to be like albums
return (
<div className={[_s.d, _s.pt25PC].join(' ')}>
<div className={containerClasses}>

View File

@ -15,55 +15,6 @@ class ToastsContainer extends React.PureComponent {
render() {
const { notifications } = this.props
console.log("notifications:", notifications)
// const notifications = [
// {
// key: '1',
// title: 'Error',
// to: 'to',
// image: 'https://gab.com/media/user/58077e8a49705.jpg',
// message: 'Unable to follow @andrew',
// date: new Date(),
// isImageAccount: true,
// },
// {
// key: '2',
// title: 'Success',
// to: 'to',
// image: 'https://gab.com/media/user/58077e8a49705.jpg',
// message: 'Your gab was posted. Click here to view',
// date: new Date(),
// isImageAccount: false,
// },
// {
// key: '3',
// title: '',
// to: 'to',
// image: 'https://gab.com/media/user/58077e8a49705.jpg',
// message: 'Unable to follow @andrew',
// date: new Date(),
// isImageAccount: true,
// },
// {
// key: '4',
// title: '',
// to: 'to',
// image: 'https://gab.com/media/user/58077e8a49705.jpg',
// message: 'Your gab was posted. Click here to view',
// date: new Date(),
// isImageAccount: false,
// },
// {
// key: '5',
// title: '',
// to: 'to',
// message: 'Your gab was deleted',
// date: new Date(),
// isImageAccount: false,
// },
// ]
const hasNotifications = !!notifications && notifications.size > 0
const containerClasses = CX({
@ -76,6 +27,7 @@ class ToastsContainer extends React.PureComponent {
pt15: 1,
heightMax100VH: 1,
pb10: 1,
saveAreaInsetMB: 1,
displayNone: !hasNotifications
})

View File

@ -67,6 +67,13 @@ class AccountAlbums extends ImmutablePureComponent {
// }
render() {
const {
account,
isMe,
} = this.props
if (!account) return null
return (
<Block>
<div className={[_s.d, _s.px10, _s.py10].join(' ')}>
@ -76,18 +83,18 @@ class AccountAlbums extends ImmutablePureComponent {
<TabBar tabs={[
{
title: 'All Photos',
to: '/'
to: `/${account.get('username')}/photos`,
},
{
title: 'Albums',
isActive: true,
to: '/'
to: `/${account.get('username')}/albums`,
},
]}/>
</div>
<div className={[_s.d, _s.w100PC, _s.flexRow, _s.flexWrap, _s.px10, _s.mb15, _s.pb10].join(' ')}>
<Album />
{ isMe && <Album isAddable /> }
<Album />
<Album />
<Album />
@ -111,7 +118,7 @@ class AccountAlbums extends ImmutablePureComponent {
// account,
// } = this.props
// if (!account) return null
// return (
// <Block>

View File

@ -0,0 +1 @@
// : todo :

View File

@ -10,6 +10,7 @@ import { me } from '../initial_state'
import {
fetchMembers,
expandMembers,
fetchGroupMembersAdminSearch,
} from '../actions/groups'
import { openPopover } from '../actions/popover'
import Account from '../components/account'
@ -21,6 +22,10 @@ import ScrollableList from '../components/scrollable_list'
class GroupMembers extends ImmutablePureComponent {
state = {
query: '',
}
componentWillMount() {
const { groupId } = this.props
@ -44,13 +49,20 @@ class GroupMembers extends ImmutablePureComponent {
this.props.onExpandMembers(this.props.groupId)
}, 300, { leading: true })
handleOnChange = (query) => {
this.setState({ query })
this.props.onChange(this.props.groupId, query)
}
render() {
const {
accountIds,
listAccountIds,
searchAcountIds,
hasMore,
group,
relationships,
} = this.props
const { query } = this.state
if (!group || !relationships) return <ColumnIndicator type='loading' />
@ -58,6 +70,8 @@ class GroupMembers extends ImmutablePureComponent {
if (!isAdminOrMod) return <ColumnIndicator type='missing' />
const accountIds = !!query ? searchAcountIds : listAccountIds
return (
<Block>
<BlockHeading title='Group Members' />
@ -67,11 +81,8 @@ class GroupMembers extends ImmutablePureComponent {
id='group-member-search'
placeholder='Search group members'
prependIcon='search'
// value={value}
onKeyUp={this.handleKeyUp}
value={query}
onChange={this.handleOnChange}
onFocus={this.handleOnFocus}
onBlur={this.handleOnBlur}
autoComplete='off'
/>
</div>
@ -113,13 +124,17 @@ const mapStateToProps = (state, { params }) => {
return {
group,
groupId,
listAccountIds: state.getIn(['user_lists', 'groups', groupId, 'items']),
searchAcountIds: state.getIn(['group_lists', 'member_search_accounts']),
relationships: state.getIn(['group_relationships', groupId]),
accountIds: state.getIn(['user_lists', 'groups', groupId, 'items']),
hasMore: !!state.getIn(['user_lists', 'groups', groupId, 'next']),
}
}
const mapDispatchToProps = (dispatch) => ({
onChange(groupId, query) {
dispatch(fetchGroupMembersAdminSearch(groupId, query))
},
onFetchMembers(groupId) {
dispatch(fetchMembers(groupId))
},

View File

@ -3,13 +3,13 @@ import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { defineMessages, injectIntl } from 'react-intl'
import debounce from 'lodash.debounce'
import isObject from 'lodash.isobject'
import {
fetchRemovedAccounts,
expandRemovedAccounts,
removeRemovedAccount,
fetchGroupRemovedAccountsAdminSearch,
} from '../actions/groups'
import { FormattedMessage } from 'react-intl'
import Account from '../components/account'
@ -21,6 +21,10 @@ import ScrollableList from '../components/scrollable_list'
class GroupRemovedAccounts extends ImmutablePureComponent {
state = {
query: '',
}
componentWillMount() {
const { groupId } = this.props
@ -37,16 +41,24 @@ class GroupRemovedAccounts extends ImmutablePureComponent {
this.props.onExpandRemovedAccounts(this.props.groupId)
}, 300, { leading: true })
handleOnChange = (query) => {
this.setState({ query })
this.props.onChange(this.props.groupId, query)
}
render() {
const {
accountIds,
listAccountIds,
searchAcountIds,
hasMore,
group,
intl,
} = this.props
const { query } = this.state
if (!group) return <ColumnIndicator type='loading' />
const accountIds = !!query ? searchAcountIds : listAccountIds
return (
<Block>
<BlockHeading title='Removed Accounts' />
@ -55,11 +67,8 @@ class GroupRemovedAccounts extends ImmutablePureComponent {
id='group-member-search'
placeholder='Search removed group members'
prependIcon='search'
// value={value}
onKeyUp={this.handleKeyUp}
value={query}
onChange={this.handleOnChange}
onFocus={this.handleOnFocus}
onBlur={this.handleOnBlur}
autoComplete='off'
/>
</div>
@ -73,11 +82,11 @@ class GroupRemovedAccounts extends ImmutablePureComponent {
{
accountIds && accountIds.map((id) => (
<Account
compact
key={id}
id={id}
actionIcon='subtract'
onActionClick={() => this.props.onRemoveRemovedAccount(group.get('id'), id)}
actionTitle={intl.formatMessage(messages.remove)}
actionTitle='Allow to join'
/>
))
}
@ -88,10 +97,6 @@ class GroupRemovedAccounts extends ImmutablePureComponent {
}
const messages = defineMessages({
remove: { id: 'groups.removed_accounts', defaultMessage: 'Allow joining' },
})
const mapStateToProps = (state, { params }) => {
const groupId = isObject(params) ? params['id'] : -1
const group = groupId === -1 ? null : state.getIn(['groups', groupId])
@ -99,12 +104,16 @@ const mapStateToProps = (state, { params }) => {
return {
group,
groupId,
accountIds: state.getIn(['user_lists', 'group_removed_accounts', groupId, 'items']),
listAccountIds: state.getIn(['user_lists', 'group_removed_accounts', groupId, 'items']),
searchAcountIds: state.getIn(['group_lists', 'removed_search_accounts']),
hasMore: !!state.getIn(['user_lists', 'group_removed_accounts', groupId, 'next']),
}
}
const mapDispatchToProps = (dispatch) => ({
onChange(groupId, query) {
dispatch(fetchGroupRemovedAccountsAdminSearch(groupId, query))
},
onFetchRemovedAccounts(groupId) {
dispatch(fetchRemovedAccounts(groupId))
},
@ -125,4 +134,4 @@ GroupRemovedAccounts.propTypes = {
onRemoveRemovedAccount: PropTypes.func.isRequired,
}
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(GroupRemovedAccounts))
export default connect(mapStateToProps, mapDispatchToProps)(GroupRemovedAccounts)

View File

@ -174,7 +174,7 @@ class ChatMessageItem extends ImmutablePureComponent {
!!expirationDate &&
<React.Fragment>
<DotTextSeperator />
<Text size='extraSmall' color='tertiary' className={_s.ml5}>Expires in {timeUntilExpiration}</Text>
<Text size='extraSmall' color='tertiary' className={_s.ml5}>Expires {timeUntilExpiration}</Text>
<Icon id='stopwatch' size='11px' className={[_s.d, _s.ml5, _s.displayInline, _s.cSecondary].join(' ')} />
</React.Fragment>
}

View File

@ -76,7 +76,7 @@ class MessagesLayout extends React.PureComponent {
actions={[
{
icon: 'add',
to: '/messages/new',
to: `'/messages/new'`,
},
{
icon: 'cog',

View File

@ -16,6 +16,10 @@ import {
GROUPS_BY_TAG_FETCH_REQUEST,
GROUPS_BY_TAG_FETCH_SUCCESS,
GROUPS_BY_TAG_FETCH_FAIL,
GROUP_MEMBERS_SEARCH_SUCCESS,
CLEAR_GROUP_MEMBERS_SEARCH,
GROUP_REMOVED_ACCOUNTS_SEARCH_SUCCESS,
CLEAR_GROUP_REMOVED_ACCOUNTS_SEARCH,
} from '../actions/groups'
import {
GROUP_TIMELINE_SORTING_TYPE_TOP,
@ -51,6 +55,8 @@ const initialState = ImmutableMap({
}),
by_category: ImmutableMap(),
by_tag: ImmutableMap(),
member_search_accounts: ImmutableList(),
removed_search_accounts: ImmutableList(),
})
export default function groupLists(state = initialState, action) {
@ -144,6 +150,14 @@ export default function groupLists(state = initialState, action) {
items: ImmutableList(),
isLoading: false,
}))
case GROUP_MEMBERS_SEARCH_SUCCESS:
return state.set('member_search_accounts', ImmutableList(action.accounts.map((item) => item.id)))
case CLEAR_GROUP_MEMBERS_SEARCH:
return state.set('member_search_accounts', ImmutableList())
case GROUP_REMOVED_ACCOUNTS_SEARCH_SUCCESS:
return state.set('removed_search_accounts', ImmutableList(action.accounts.map((item) => item.id)))
case CLEAR_GROUP_REMOVED_ACCOUNTS_SEARCH:
return state.set('removed_search_accounts', ImmutableList())
default:
return state
}

View File

@ -404,7 +404,7 @@ class Account < ApplicationRecord
records
end
def advanced_search_for(terms, account, limit = 10, following = false, offset = 0, options = {})
def advanced_search_for(terms, account, limit = 10, offset = 0, options = {})
textsearch, query = generate_query_for_search(terms)
@onlyVerified = options[:onlyVerified] || false
@ -426,7 +426,6 @@ class Account < ApplicationRecord
records = find_by_sql([sql, account.id, account.id, limit, offset])
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
records
end

View File

@ -70,6 +70,16 @@ class Group < ApplicationRecord
.limit(25)
.offset(offset)
end
def search_for_members(group, term, limit)
pattern = '%' + sanitize_sql_like(term.strip) + '%'
group.accounts.where("LOWER(username) LIKE LOWER(?)", pattern).limit(limit)
end
def search_for_removed_accounts(group, term, limit)
pattern = '%' + sanitize_sql_like(term.strip) + '%'
group.removed_accounts.where("LOWER(username) LIKE LOWER(?)", pattern).limit(limit)
end
end
def has_password?

View File

@ -4,13 +4,13 @@ class AccountSearchService < BaseService
attr_reader :query, :limit, :offset, :options, :account
def call(query, account = nil, options = {})
puts "query:"+query.inspect
@query = query.strip
@limit = options[:limit].to_i
@offset = options[:offset].to_i
@onlyVerified = options[:onlyVerified] || false
@options = options
@account = account
@group = options[:group] || nil
search_service_results
end
@ -84,7 +84,7 @@ class AccountSearchService < BaseService
end
def advanced_search_results
Account.advanced_search_for(terms_for_query, account, limit, options[:following], offset, onlyVerified: @onlyVerified)
Account.advanced_search_for(terms_for_query, account, limit, offset, onlyVerified: @onlyVerified)
end
def simple_search_results

View File

@ -349,6 +349,9 @@ Rails.application.routes.draw do
member do
delete '/statuses/:status_id', to: 'groups#destroy_status'
post '/statuses/:status_id/approve', to: 'groups#approve_status'
get '/member_search', to: 'groups#member_search'
get '/removed_accounts_search', to: 'groups#removed_accounts_search'
end
get '/category/:category', to: 'groups#by_category'