[Glitch] Add profile directory to web UI
Port 79922ae20f
to glitch-soc
Signed-off-by: Thibaut Girka <thib@sitedethib.com>
This commit is contained in:
parent
fce59abade
commit
2cdbfb39e8
18 changed files with 691 additions and 101 deletions
61
app/javascript/flavours/glitch/actions/directory.js
Normal file
61
app/javascript/flavours/glitch/actions/directory.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import api from 'flavours/glitch/util/api';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { fetchRelationships } from './accounts';
|
||||
|
||||
export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
|
||||
export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
|
||||
export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL';
|
||||
|
||||
export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
|
||||
export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
|
||||
export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL';
|
||||
|
||||
export const fetchDirectory = params => (dispatch, getState) => {
|
||||
dispatch(fetchDirectoryRequest());
|
||||
|
||||
api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchDirectorySuccess(data));
|
||||
dispatch(fetchRelationships(data.map(x => x.id)));
|
||||
}).catch(error => dispatch(fetchDirectoryFail(error)));
|
||||
};
|
||||
|
||||
export const fetchDirectoryRequest = () => ({
|
||||
type: DIRECTORY_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchDirectorySuccess = accounts => ({
|
||||
type: DIRECTORY_FETCH_SUCCESS,
|
||||
accounts,
|
||||
});
|
||||
|
||||
export const fetchDirectoryFail = error => ({
|
||||
type: DIRECTORY_FETCH_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const expandDirectory = params => (dispatch, getState) => {
|
||||
dispatch(expandDirectoryRequest());
|
||||
|
||||
const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
|
||||
|
||||
api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(expandDirectorySuccess(data));
|
||||
dispatch(fetchRelationships(data.map(x => x.id)));
|
||||
}).catch(error => dispatch(expandDirectoryFail(error)));
|
||||
};
|
||||
|
||||
export const expandDirectoryRequest = () => ({
|
||||
type: DIRECTORY_EXPAND_REQUEST,
|
||||
});
|
||||
|
||||
export const expandDirectorySuccess = accounts => ({
|
||||
type: DIRECTORY_EXPAND_SUCCESS,
|
||||
accounts,
|
||||
});
|
||||
|
||||
export const expandDirectoryFail = error => ({
|
||||
type: DIRECTORY_EXPAND_FAIL,
|
||||
error,
|
||||
});
|
35
app/javascript/flavours/glitch/components/radio_button.js
Normal file
35
app/javascript/flavours/glitch/components/radio_button.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class RadioButton extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
label: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { name, value, checked, onChange, label } = this.props;
|
||||
|
||||
return (
|
||||
<label className='radio-button'>
|
||||
<input
|
||||
name={name}
|
||||
type='radio'
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<span className={classNames('radio-button__input', { checked })} />
|
||||
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeGetAccount } from 'flavours/glitch/selectors';
|
||||
import Avatar from 'flavours/glitch/components/avatar';
|
||||
import DisplayName from 'flavours/glitch/components/display_name';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/util/initial_state';
|
||||
import { shortNumberFormat } from 'flavours/glitch/util/numbers';
|
||||
import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
|
||||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
account: getAccount(state, id),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onFollow (account) {
|
||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||
if (unfollowModal) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.unfollowConfirm),
|
||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||
}));
|
||||
} else {
|
||||
dispatch(unfollowAccount(account.get('id')));
|
||||
}
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onBlock (account) {
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onMute (account) {
|
||||
if (account.getIn(['relationship', 'muting'])) {
|
||||
dispatch(unmuteAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(initMuteModal(account));
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
@connect(makeMapStateToProps, mapDispatchToProps)
|
||||
class AccountCard extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleFollow = () => {
|
||||
this.props.onFollow(this.props.account);
|
||||
}
|
||||
|
||||
handleBlock = () => {
|
||||
this.props.onBlock(this.props.account);
|
||||
}
|
||||
|
||||
handleMute = () => {
|
||||
this.props.onMute(this.props.account);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, intl } = this.props;
|
||||
|
||||
let buttons;
|
||||
|
||||
if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||
const following = account.getIn(['relationship', 'following']);
|
||||
const requested = account.getIn(['relationship', 'requested']);
|
||||
const blocking = account.getIn(['relationship', 'blocking']);
|
||||
const muting = account.getIn(['relationship', 'muting']);
|
||||
|
||||
if (requested) {
|
||||
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
|
||||
} else if (blocking) {
|
||||
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||
} else if (muting) {
|
||||
buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
|
||||
} else if (!account.get('moved') || following) {
|
||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='directory__card'>
|
||||
<div className='directory__card__img'>
|
||||
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
|
||||
</div>
|
||||
|
||||
<div className='directory__card__bar'>
|
||||
<Permalink className='directory__card__bar__name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||
<Avatar account={account} size={48} />
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
<div className='directory__card__bar__relationship account__relationship'>
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='directory__card__extra'>
|
||||
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} />}
|
||||
</div>
|
||||
|
||||
<div className='directory__card__extra'>
|
||||
<div className='accounts-table__count'>{shortNumberFormat(account.get('statuses_count'))} <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div>
|
||||
<div className='accounts-table__count'>{shortNumberFormat(account.get('followers_count'))} <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div>
|
||||
<div className='accounts-table__count'>{account.get('last_status_at') === null ? <FormattedMessage id='account.never_active' defaultMessage='Never' /> : <RelativeTimestamp timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
171
app/javascript/flavours/glitch/features/directory/index.js
Normal file
171
app/javascript/flavours/glitch/features/directory/index.js
Normal file
|
@ -0,0 +1,171 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Column from 'flavours/glitch/components/column';
|
||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||
import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavours/glitch/actions/columns';
|
||||
import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import AccountCard from './components/account_card';
|
||||
import RadioButton from 'flavours/glitch/components/radio_button';
|
||||
import classNames from 'classnames';
|
||||
import LoadMore from 'flavours/glitch/components/load_more';
|
||||
import { ScrollContainer } from 'react-router-scroll-4';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
|
||||
recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
|
||||
newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
|
||||
local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
|
||||
federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()),
|
||||
isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true),
|
||||
domain: state.getIn(['meta', 'domain']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Directory extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
shouldUpdateScroll: PropTypes.func,
|
||||
columnId: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
domain: PropTypes.string.isRequired,
|
||||
params: PropTypes.shape({
|
||||
order: PropTypes.string,
|
||||
local: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
state = {
|
||||
order: null,
|
||||
local: null,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state)));
|
||||
}
|
||||
}
|
||||
|
||||
getParams = (props, state) => ({
|
||||
order: state.order === null ? (props.params.order || 'active') : state.order,
|
||||
local: state.local === null ? (props.params.local || false) : state.local,
|
||||
});
|
||||
|
||||
handleMove = dir => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
}
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchDirectory(this.getParams(this.props, this.state)));
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
const { dispatch } = this.props;
|
||||
const paramsOld = this.getParams(prevProps, prevState);
|
||||
const paramsNew = this.getParams(this.props, this.state);
|
||||
|
||||
if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) {
|
||||
dispatch(fetchDirectory(paramsNew));
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
}
|
||||
|
||||
handleChangeOrder = e => {
|
||||
const { dispatch, columnId } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(changeColumnParams(columnId, ['order'], e.target.value));
|
||||
} else {
|
||||
this.setState({ order: e.target.value });
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeLocal = e => {
|
||||
const { dispatch, columnId } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(changeColumnParams(columnId, ['local'], e.target.value === '1'));
|
||||
} else {
|
||||
this.setState({ local: e.target.value === '1' });
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(expandDirectory(this.getParams(this.props, this.state)));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isLoading, accountIds, intl, columnId, multiColumn, domain, shouldUpdateScroll } = this.props;
|
||||
const { order, local } = this.getParams(this.props, this.state);
|
||||
const pinned = !!columnId;
|
||||
|
||||
const scrollableArea = (
|
||||
<div className='scrollable' style={{ background: 'transparent' }}>
|
||||
<div className='filter-form'>
|
||||
<div className='filter-form__column' role='group'>
|
||||
<RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
|
||||
<RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} />
|
||||
</div>
|
||||
|
||||
<div className='filter-form__column' role='group'>
|
||||
<RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain })} checked={local} onChange={this.handleChangeLocal} />
|
||||
<RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classNames('directory__list', { loading: isLoading })}>
|
||||
{accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
|
||||
</div>
|
||||
|
||||
<LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
icon='address-book-o'
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
|
||||
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory' shouldUpdateScroll={shouldUpdateScroll}>{scrollableArea}</ScrollContainer> : scrollableArea}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -8,7 +8,7 @@ import { openModal } from 'flavours/glitch/actions/modal';
|
|||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me } from 'flavours/glitch/util/initial_state';
|
||||
import { me, profile_directory } from 'flavours/glitch/util/initial_state';
|
||||
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
|
@ -36,6 +36,7 @@ const messages = defineMessages({
|
|||
lists_subheading: { id: 'column_subheading.lists', defaultMessage: 'Lists' },
|
||||
misc: { id: 'navigation_bar.misc', defaultMessage: 'Misc' },
|
||||
menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
profile_directory: { id: 'getting_started.directory', defaultMessage: 'Profile directory' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
|
@ -150,13 +151,17 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
|||
navItems.push(<ColumnLink key='6' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||
}
|
||||
|
||||
navItems.push(<ColumnLink key='7' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
|
||||
if (profile_directory) {
|
||||
navItems.push(<ColumnLink key='7' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />);
|
||||
}
|
||||
|
||||
navItems.push(<ColumnLink key='8' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
|
||||
|
||||
listItems = listItems.concat([
|
||||
<div key='8'>
|
||||
<ColumnLink key='9' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
<div key='9'>
|
||||
<ColumnLink key='10' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
{lists.map(list =>
|
||||
<ColumnLink key={(9 + Number(list.get('id'))).toString()} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
|
||||
<ColumnLink key={(11 + Number(list.get('id'))).toString()} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
|
||||
)}
|
||||
</div>,
|
||||
]);
|
||||
|
|
|
@ -20,7 +20,7 @@ const mapDispatchToProps = (dispatch, { columnId }) => ({
|
|||
},
|
||||
|
||||
onLoad (value) {
|
||||
return api().get('/api/v2/search', { params: { q: value } }).then(response => {
|
||||
return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
|
||||
return (response.data.hashtags || []).map((tag) => {
|
||||
return { value: tag.name, label: `#${tag.name}` };
|
||||
});
|
||||
|
|
|
@ -12,7 +12,19 @@ import BundleContainer from '../containers/bundle_container';
|
|||
import ColumnLoading from './column_loading';
|
||||
import DrawerLoading from './drawer_loading';
|
||||
import BundleColumnError from './bundle_column_error';
|
||||
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, BookmarkedStatuses, ListTimeline } from 'flavours/glitch/util/async-components';
|
||||
import {
|
||||
Compose,
|
||||
Notifications,
|
||||
HomeTimeline,
|
||||
CommunityTimeline,
|
||||
PublicTimeline,
|
||||
HashtagTimeline,
|
||||
DirectTimeline,
|
||||
FavouritedStatuses,
|
||||
BookmarkedStatuses,
|
||||
ListTimeline,
|
||||
Directory,
|
||||
} from 'flavours/glitch/util/async-components';
|
||||
import ComposePanel from './compose_panel';
|
||||
import NavigationPanel from './navigation_panel';
|
||||
|
||||
|
@ -30,6 +42,7 @@ const componentMap = {
|
|||
'FAVOURITES': FavouritedStatuses,
|
||||
'BOOKMARKS': BookmarkedStatuses,
|
||||
'LIST': ListTimeline,
|
||||
'DIRECTORY': Directory,
|
||||
};
|
||||
|
||||
const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/);
|
||||
|
|
|
@ -16,6 +16,7 @@ const NavigationPanel = ({ onOpenSettings }) => (
|
|||
<NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' icon='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
|
||||
<NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' icon='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
|
||||
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' icon='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
|
||||
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' icon='address-book-o' fixedWidth /><FormattedMessage id='getting_started.profile_directory' defaultMessage='Profile directory' /></NavLink>}
|
||||
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' icon='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
|
||||
|
||||
<ListPanel />
|
||||
|
@ -25,7 +26,6 @@ const NavigationPanel = ({ onOpenSettings }) => (
|
|||
<a className='column-link column-link--transparent' href='/settings/preferences' target='_blank'><Icon className='column-link__icon' icon='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>
|
||||
<a className='column-link column-link--transparent' href='#' onClick={onOpenSettings}><Icon className='column-link__icon' icon='cogs' fixedWidth /><FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' /></a>
|
||||
<a className='column-link column-link--transparent' href='/relationships' target='_blank'><Icon className='column-link__icon' icon='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
|
||||
{!!profile_directory && <a className='column-link column-link--transparent' href='/explore'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></a>}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ import {
|
|||
Lists,
|
||||
Search,
|
||||
GettingStartedMisc,
|
||||
Directory,
|
||||
} from 'flavours/glitch/util/async-components';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import { me } from 'flavours/glitch/util/initial_state';
|
||||
|
@ -185,6 +186,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
|
||||
|
||||
<WrappedRoute path='/search' component={Search} content={children} />
|
||||
<WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||
|
||||
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
|
||||
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
|
||||
|
|
|
@ -20,6 +20,14 @@ import {
|
|||
MUTES_FETCH_SUCCESS,
|
||||
MUTES_EXPAND_SUCCESS,
|
||||
} from 'flavours/glitch/actions/mutes';
|
||||
import {
|
||||
DIRECTORY_FETCH_REQUEST,
|
||||
DIRECTORY_FETCH_SUCCESS,
|
||||
DIRECTORY_FETCH_FAIL,
|
||||
DIRECTORY_EXPAND_REQUEST,
|
||||
DIRECTORY_EXPAND_SUCCESS,
|
||||
DIRECTORY_EXPAND_FAIL,
|
||||
} from 'flavours/glitch/actions/directory';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
|
@ -74,6 +82,16 @@ export default function userLists(state = initialState, action) {
|
|||
return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
case MUTES_EXPAND_SUCCESS:
|
||||
return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
case DIRECTORY_FETCH_SUCCESS:
|
||||
return state.setIn(['directory', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false);
|
||||
case DIRECTORY_EXPAND_SUCCESS:
|
||||
return state.updateIn(['directory', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false);
|
||||
case DIRECTORY_FETCH_REQUEST:
|
||||
case DIRECTORY_EXPAND_REQUEST:
|
||||
return state.setIn(['directory', 'isLoading'], true);
|
||||
case DIRECTORY_FETCH_FAIL:
|
||||
case DIRECTORY_EXPAND_FAIL:
|
||||
return state.setIn(['directory', 'isLoading'], false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -415,6 +415,24 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.directory__section-headline {
|
||||
background: darken($ui-base-color, 2%);
|
||||
border-bottom-color: transparent;
|
||||
|
||||
a,
|
||||
button {
|
||||
&.active {
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-color: transparent transparent darken($ui-base-color, 7%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account__moved-note {
|
||||
|
|
180
app/javascript/flavours/glitch/styles/components/directory.scss
Normal file
180
app/javascript/flavours/glitch/styles/components/directory.scss
Normal file
|
@ -0,0 +1,180 @@
|
|||
.directory {
|
||||
&__list {
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
transition: opacity 100ms ease-in;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__card {
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&__img {
|
||||
height: 125px;
|
||||
position: relative;
|
||||
background: darken($ui-base-color, 12%);
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
padding: 10px;
|
||||
|
||||
&__name {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__relationship {
|
||||
width: 23px;
|
||||
min-height: 1px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex: 0 0 auto;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
padding-top: 2px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
background: darken($ui-base-color, 8%);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.display-name {
|
||||
margin-left: 15px;
|
||||
text-align: left;
|
||||
|
||||
strong {
|
||||
font-size: 15px;
|
||||
color: $primary-text-color;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: $darker-text-color;
|
||||
font-weight: 400;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__extra {
|
||||
background: $ui-base-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.accounts-table__count {
|
||||
width: 33.33%;
|
||||
flex: 0 0 auto;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.account__header__content {
|
||||
box-sizing: border-box;
|
||||
padding: 15px 10px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
width: 100%;
|
||||
min-height: 18px + 30px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
p {
|
||||
display: none;
|
||||
|
||||
&:first-child {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
br {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
background: $ui-base-color;
|
||||
|
||||
&__column {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 6px 0;
|
||||
line-height: 18px;
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
|
||||
input[type=radio],
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__input {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border: 1px solid $ui-primary-color;
|
||||
box-sizing: border-box;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 10px;
|
||||
top: -1px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
|
||||
&.checked {
|
||||
border-color: lighten($ui-highlight-color, 8%);
|
||||
background: lighten($ui-highlight-color, 8%);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1467,6 +1467,7 @@ noscript {
|
|||
@import 'composer';
|
||||
@import 'columns';
|
||||
@import 'regeneration_indicator';
|
||||
@import 'directory';
|
||||
@import 'search';
|
||||
@import 'emoji';
|
||||
@import 'doodle';
|
||||
|
|
|
@ -669,38 +669,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.detailed,
|
||||
&.fullscreen {
|
||||
.video-player__buttons {
|
||||
button {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
&.detailed,
|
||||
&.fullscreen {
|
||||
.video-player__buttons {
|
||||
button {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-spoiler-video {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
cursor: pointer;
|
||||
margin-top: 8px;
|
||||
position: relative;
|
||||
|
||||
@include fullwidth-gallery;
|
||||
|
||||
border: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.media-spoiler-video-play-icon {
|
||||
border-radius: 100px;
|
||||
color: rgba($primary-text-color, 0.8);
|
||||
font-size: 36px;
|
||||
left: 50%;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
|
|
@ -83,6 +83,24 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.directory__list {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.directory__card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.autosuggest-textarea__textarea {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
|
|
@ -886,67 +886,6 @@ a.status-card.compact:hover {
|
|||
background-position: center center;
|
||||
}
|
||||
|
||||
.status__video-player {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $base-shadow-color;
|
||||
box-sizing: border-box;
|
||||
cursor: default; /* May not be needed */
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
@include fullwidth-gallery;
|
||||
}
|
||||
|
||||
.status__video-player-video {
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
&:not(.letterbox) {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.status__video-player-expand,
|
||||
.status__video-player-mute {
|
||||
color: $primary-text-color;
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
|
||||
}
|
||||
|
||||
.status__video-player-spoiler {
|
||||
display: none;
|
||||
color: $primary-text-color;
|
||||
left: 4px;
|
||||
position: absolute;
|
||||
text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
|
||||
top: 4px;
|
||||
z-index: 100;
|
||||
|
||||
&.status__video-player-spoiler--visible {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.status__video-player-expand {
|
||||
bottom: 4px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.status__video-player-mute {
|
||||
top: 4px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
|
|
|
@ -161,3 +161,7 @@ export function Search () {
|
|||
export function Tesseract () {
|
||||
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
|
||||
}
|
||||
|
||||
export function Directory () {
|
||||
return import(/* webpackChunkName: "features/glitch/async/directory" */'flavours/glitch/features/directory');
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export const pollLimits = (initialState && initialState.poll_limits);
|
|||
export const invitesEnabled = getMeta('invites_enabled');
|
||||
export const version = getMeta('version');
|
||||
export const mascot = getMeta('mascot');
|
||||
export const profile_directory = getMeta('profile_directory');
|
||||
export const isStaff = getMeta('is_staff');
|
||||
export const defaultContentType = getMeta('default_content_type');
|
||||
export const forceSingleColumn = getMeta('advanced_layout') === false;
|
||||
|
|
Loading…
Reference in a new issue