Add emoji overlay to avatars in reaction list
I swear there has to be a better way to do this
This commit is contained in:
parent
a4bfa67151
commit
31406cb0f2
9 changed files with 138 additions and 42 deletions
|
@ -303,7 +303,7 @@ export function fetchReactions(id) {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
const accounts = response.data.map(item => item.account);
|
const accounts = response.data.map(item => item.account);
|
||||||
dispatch(importFetchedAccounts(accounts));
|
dispatch(importFetchedAccounts(accounts));
|
||||||
dispatch(fetchReactionsSuccess(id, accounts, next ? next.uri : null));
|
dispatch(fetchReactionsSuccess(id, response.data, next ? next.uri : null));
|
||||||
dispatch(fetchRelationships(accounts.map(item => item.id)));
|
dispatch(fetchRelationships(accounts.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchReactionsFail(id, error));
|
dispatch(fetchReactionsFail(id, error));
|
||||||
|
@ -318,11 +318,11 @@ export function fetchReactionsRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchReactionsSuccess(id, accounts, next) {
|
export function fetchReactionsSuccess(id, reactions, next) {
|
||||||
return {
|
return {
|
||||||
type: REACTIONS_FETCH_SUCCESS,
|
type: REACTIONS_FETCH_SUCCESS,
|
||||||
id,
|
id,
|
||||||
accounts,
|
reactions,
|
||||||
next,
|
next,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -349,7 +349,7 @@ export function expandReactions(id) {
|
||||||
const accounts = response.data.map(item => item.account);
|
const accounts = response.data.map(item => item.account);
|
||||||
|
|
||||||
dispatch(importFetchedAccounts(accounts));
|
dispatch(importFetchedAccounts(accounts));
|
||||||
dispatch(expandReactionsSuccess(id, accounts, next ? next.uri : null));
|
dispatch(expandReactionsSuccess(id, response.data, next ? next.uri : null));
|
||||||
dispatch(fetchRelationships(accounts.map(item => item.id)));
|
dispatch(fetchRelationships(accounts.map(item => item.id)));
|
||||||
}).catch(error => dispatch(expandReactionsFail(id, error)));
|
}).catch(error => dispatch(expandReactionsFail(id, error)));
|
||||||
};
|
};
|
||||||
|
@ -362,11 +362,11 @@ export function expandReactionsRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function expandReactionsSuccess(id, accounts, next) {
|
export function expandReactionsSuccess(id, reactions, next) {
|
||||||
return {
|
return {
|
||||||
type: REACTIONS_EXPAND_SUCCESS,
|
type: REACTIONS_EXPAND_SUCCESS,
|
||||||
id,
|
id,
|
||||||
accounts,
|
reactions,
|
||||||
next,
|
next,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
5
app/javascript/flavours/glitch/api_types/reaction.ts
Normal file
5
app/javascript/flavours/glitch/api_types/reaction.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export interface ApiStatusReactionJSON {
|
||||||
|
name: string;
|
||||||
|
static_url?: string | undefined;
|
||||||
|
url?: string | undefined;
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import { VerifiedBadge } from 'flavours/glitch/components/verified_badge';
|
||||||
import { me } from '../initial_state';
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
import { Avatar } from './avatar';
|
import { Avatar } from './avatar';
|
||||||
|
import { AvatarOverlay } from './avatar_overlay';
|
||||||
import { Button } from './button';
|
import { Button } from './button';
|
||||||
import { FollowersCounter } from './counters';
|
import { FollowersCounter } from './counters';
|
||||||
import { DisplayName } from './display_name';
|
import { DisplayName } from './display_name';
|
||||||
|
@ -42,6 +43,7 @@ class Account extends ImmutablePureComponent {
|
||||||
onMute: PropTypes.func,
|
onMute: PropTypes.func,
|
||||||
onMuteNotifications: PropTypes.func,
|
onMuteNotifications: PropTypes.func,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
overlayEmoji: PropTypes.object,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
minimal: PropTypes.bool,
|
minimal: PropTypes.bool,
|
||||||
defaultAction: PropTypes.string,
|
defaultAction: PropTypes.string,
|
||||||
|
@ -50,6 +52,7 @@ class Account extends ImmutablePureComponent {
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
size: 46,
|
size: 46,
|
||||||
|
overlayEmoji: { name: null }
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFollow = () => {
|
handleFollow = () => {
|
||||||
|
@ -73,7 +76,7 @@ class Account extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props;
|
const { account, intl, hidden, withBio, defaultAction, overlayEmoji, size, minimal } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return <EmptyAccount size={size} minimal={minimal} />;
|
return <EmptyAccount size={size} minimal={minimal} />;
|
||||||
|
@ -138,12 +141,19 @@ class Account extends ImmutablePureComponent {
|
||||||
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
|
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let statusAvatar;
|
||||||
|
if (!overlayEmoji.name) {
|
||||||
|
statusAvatar = <Avatar account={account} size={size} />;
|
||||||
|
} else {
|
||||||
|
statusAvatar = <AvatarOverlay account={account} emoji={overlayEmoji} baseSize={size} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('account', { 'account--minimal': minimal })}>
|
<div className={classNames('account', { 'account--minimal': minimal })}>
|
||||||
<div className='account__wrapper'>
|
<div className='account__wrapper'>
|
||||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
||||||
<div className='account__avatar-wrapper'>
|
<div className='account__avatar-wrapper'>
|
||||||
<Avatar account={account} size={size} />
|
{statusAvatar}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account__contents'>
|
<div className='account__contents'>
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import type { Account } from 'flavours/glitch/models/account';
|
import type { Account } from 'flavours/glitch/models/account';
|
||||||
|
import type { StatusReaction } from 'flavours/glitch/models/reaction';
|
||||||
|
|
||||||
import { useHovering } from '../hooks/useHovering';
|
import { useHovering } from '../hooks/useHovering';
|
||||||
import { autoPlayGif } from '../initial_state';
|
import { autoPlayGif } from '../initial_state';
|
||||||
|
|
||||||
|
import { Emoji } from './status_reactions';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||||
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
friend?: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||||
|
emoji?: StatusReaction | undefined;
|
||||||
size?: number;
|
size?: number;
|
||||||
baseSize?: number;
|
baseSize?: number;
|
||||||
overlaySize?: number;
|
overlaySize?: number;
|
||||||
|
@ -14,6 +18,7 @@ interface Props {
|
||||||
export const AvatarOverlay: React.FC<Props> = ({
|
export const AvatarOverlay: React.FC<Props> = ({
|
||||||
account,
|
account,
|
||||||
friend,
|
friend,
|
||||||
|
emoji,
|
||||||
size = 46,
|
size = 46,
|
||||||
baseSize = 36,
|
baseSize = 36,
|
||||||
overlaySize = 24,
|
overlaySize = 24,
|
||||||
|
@ -27,6 +32,32 @@ export const AvatarOverlay: React.FC<Props> = ({
|
||||||
? friend?.get('avatar')
|
? friend?.get('avatar')
|
||||||
: friend?.get('avatar_static');
|
: friend?.get('avatar_static');
|
||||||
|
|
||||||
|
let overlayElement;
|
||||||
|
if (friendSrc) {
|
||||||
|
overlayElement = (
|
||||||
|
<div
|
||||||
|
className='account__avatar'
|
||||||
|
style={{ width: `${overlaySize}px`, height: `${overlaySize}px` }}
|
||||||
|
data-avatar-of={`@${friend?.get('acct')}`}
|
||||||
|
>
|
||||||
|
{friendSrc && <img src={friendSrc} alt={friend?.get('acct')} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
overlayElement = (
|
||||||
|
<div className='account__emoji' data-emoji-name={emoji?.name}>
|
||||||
|
{emoji && (
|
||||||
|
<Emoji
|
||||||
|
emoji={emoji.name}
|
||||||
|
hovered={hovering}
|
||||||
|
url={emoji.url}
|
||||||
|
staticUrl={emoji.static_url}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='account__avatar-overlay'
|
className='account__avatar-overlay'
|
||||||
|
@ -43,15 +74,7 @@ export const AvatarOverlay: React.FC<Props> = ({
|
||||||
{accountSrc && <img src={accountSrc} alt={account?.get('acct')} />}
|
{accountSrc && <img src={accountSrc} alt={account?.get('acct')} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='account__avatar-overlay-overlay'>
|
<div className='account__avatar-overlay-overlay'>{overlayElement}</div>
|
||||||
<div
|
|
||||||
className='account__avatar'
|
|
||||||
style={{ width: `${overlaySize}px`, height: `${overlaySize}px` }}
|
|
||||||
data-avatar-of={`@${friend?.get('acct')}`}
|
|
||||||
>
|
|
||||||
{friendSrc && <img src={friendSrc} alt={friend?.get('acct')} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,9 +27,9 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
accountIds: state.getIn(['user_lists', 'reactions', props.params.statusId, 'items']),
|
reactions: state.getIn(['status_reactions', 'reactions', props.params.statusId, 'items']),
|
||||||
hasMore: !!state.getIn(['user_lists', 'reactions', props.params.statusId, 'next']),
|
hasMore: !!state.getIn(['status_reactions', 'reactions', props.params.statusId, 'next']),
|
||||||
isLoading: state.getIn(['user_lists', 'reactions', props.params.statusId, 'isLoading'], true),
|
isLoading: state.getIn(['status_reactions', 'reactions', props.params.statusId, 'isLoading'], true),
|
||||||
});
|
});
|
||||||
|
|
||||||
class Reactions extends ImmutablePureComponent {
|
class Reactions extends ImmutablePureComponent {
|
||||||
|
@ -37,7 +37,7 @@ class Reactions extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
accountIds: ImmutablePropTypes.list,
|
reactions: ImmutablePropTypes.orderedSet,
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
@ -67,9 +67,9 @@ class Reactions extends ImmutablePureComponent {
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
|
const { intl, reactions, hasMore, isLoading, multiColumn } = this.props;
|
||||||
|
|
||||||
if (!accountIds) {
|
if (!reactions) {
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
|
@ -77,6 +77,9 @@ class Reactions extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accountIds = reactions.map(v => v.account);
|
||||||
|
const reactionsByAccount = new Map(reactions.map(v => [v.account, v]));
|
||||||
|
|
||||||
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
|
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -102,7 +105,7 @@ class Reactions extends ImmutablePureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{accountIds.map(id =>
|
{accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} withNote={false} />,
|
<AccountContainer key={id} id={id} withNote={false} overlayEmoji={reactionsByAccount.get(id)} />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
|
|
||||||
|
|
13
app/javascript/flavours/glitch/models/reaction.ts
Normal file
13
app/javascript/flavours/glitch/models/reaction.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import type { RecordOf } from 'immutable';
|
||||||
|
import { Record } from 'immutable';
|
||||||
|
|
||||||
|
import type { ApiStatusReactionJSON } from 'flavours/glitch/api_types/reaction';
|
||||||
|
|
||||||
|
type StatusReactionShape = Required<ApiStatusReactionJSON>;
|
||||||
|
export type StatusReaction = RecordOf<StatusReactionShape>;
|
||||||
|
|
||||||
|
export const CustomEmojiFactory = Record<StatusReactionShape>({
|
||||||
|
name: '',
|
||||||
|
static_url: '',
|
||||||
|
url: '',
|
||||||
|
});
|
|
@ -38,6 +38,7 @@ import search from './search';
|
||||||
import server from './server';
|
import server from './server';
|
||||||
import settings from './settings';
|
import settings from './settings';
|
||||||
import status_lists from './status_lists';
|
import status_lists from './status_lists';
|
||||||
|
import status_reactions from './status_reactions';
|
||||||
import statuses from './statuses';
|
import statuses from './statuses';
|
||||||
import suggestions from './suggestions';
|
import suggestions from './suggestions';
|
||||||
import tags from './tags';
|
import tags from './tags';
|
||||||
|
@ -86,6 +87,7 @@ const reducers = {
|
||||||
history,
|
history,
|
||||||
tags,
|
tags,
|
||||||
followed_tags,
|
followed_tags,
|
||||||
|
status_reactions,
|
||||||
notificationPolicy: notificationPolicyReducer,
|
notificationPolicy: notificationPolicyReducer,
|
||||||
notificationRequests: notificationRequestsReducer,
|
notificationRequests: notificationRequestsReducer,
|
||||||
};
|
};
|
||||||
|
|
57
app/javascript/flavours/glitch/reducers/status_reactions.js
Normal file
57
app/javascript/flavours/glitch/reducers/status_reactions.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
|
|
||||||
|
import {
|
||||||
|
REACTIONS_FETCH_SUCCESS,
|
||||||
|
REACTIONS_EXPAND_SUCCESS,
|
||||||
|
REACTIONS_FETCH_REQUEST,
|
||||||
|
REACTIONS_EXPAND_REQUEST,
|
||||||
|
REACTIONS_FETCH_FAIL,
|
||||||
|
REACTIONS_EXPAND_FAIL,
|
||||||
|
} from '../actions/interactions';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap({
|
||||||
|
reactions: ImmutableMap({
|
||||||
|
next: null,
|
||||||
|
isLoading: false,
|
||||||
|
items: ImmutableOrderedSet(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizeList = (state, path, reactions, next) => {
|
||||||
|
const filteredReactions = reactions.map(v => {
|
||||||
|
v.account = v.account.id;
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
return state.setIn(path, ImmutableMap({
|
||||||
|
next,
|
||||||
|
items: ImmutableOrderedSet(filteredReactions),
|
||||||
|
isLoading: false,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendToList = (state, path, reactions, next) => {
|
||||||
|
const filteredReactions = reactions.map(v => {
|
||||||
|
v.account = v.account.id;
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
return state.updateIn(path, map => {
|
||||||
|
return map.set('next', next).set('isLoading', false).update('items', list => list.concat(filteredReactions));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function statusReactions(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case REACTIONS_FETCH_SUCCESS:
|
||||||
|
return normalizeList(state, ['reactions', action.id], action.reactions, action.next);
|
||||||
|
case REACTIONS_EXPAND_SUCCESS:
|
||||||
|
return appendToList(state, ['reactions', action.id], action.reactions, action.next);
|
||||||
|
case REACTIONS_FETCH_REQUEST:
|
||||||
|
case REACTIONS_EXPAND_REQUEST:
|
||||||
|
return state.setIn(['reactions', action.id, 'isLoading'], true);
|
||||||
|
case REACTIONS_FETCH_FAIL:
|
||||||
|
case REACTIONS_EXPAND_FAIL:
|
||||||
|
return state.setIn(['reactions', action.id, 'isLoading'], false);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,12 +57,6 @@ import {
|
||||||
FAVOURITES_EXPAND_REQUEST,
|
FAVOURITES_EXPAND_REQUEST,
|
||||||
FAVOURITES_EXPAND_SUCCESS,
|
FAVOURITES_EXPAND_SUCCESS,
|
||||||
FAVOURITES_EXPAND_FAIL,
|
FAVOURITES_EXPAND_FAIL,
|
||||||
REACTIONS_FETCH_SUCCESS,
|
|
||||||
REACTIONS_EXPAND_SUCCESS,
|
|
||||||
REACTIONS_FETCH_REQUEST,
|
|
||||||
REACTIONS_EXPAND_REQUEST,
|
|
||||||
REACTIONS_FETCH_FAIL,
|
|
||||||
REACTIONS_EXPAND_FAIL,
|
|
||||||
} from '../actions/interactions';
|
} from '../actions/interactions';
|
||||||
import {
|
import {
|
||||||
MUTES_FETCH_REQUEST,
|
MUTES_FETCH_REQUEST,
|
||||||
|
@ -83,7 +77,6 @@ const initialListState = ImmutableMap({
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
followers: initialListState,
|
followers: initialListState,
|
||||||
following: initialListState,
|
following: initialListState,
|
||||||
reactions: initialListState,
|
|
||||||
reblogged_by: initialListState,
|
reblogged_by: initialListState,
|
||||||
favourited_by: initialListState,
|
favourited_by: initialListState,
|
||||||
follow_requests: initialListState,
|
follow_requests: initialListState,
|
||||||
|
@ -146,16 +139,6 @@ export default function userLists(state = initialState, action) {
|
||||||
case FOLLOWING_FETCH_FAIL:
|
case FOLLOWING_FETCH_FAIL:
|
||||||
case FOLLOWING_EXPAND_FAIL:
|
case FOLLOWING_EXPAND_FAIL:
|
||||||
return state.setIn(['following', action.id, 'isLoading'], false);
|
return state.setIn(['following', action.id, 'isLoading'], false);
|
||||||
case REACTIONS_FETCH_SUCCESS:
|
|
||||||
return normalizeList(state, ['reactions', action.id], action.accounts, action.next);
|
|
||||||
case REACTIONS_EXPAND_SUCCESS:
|
|
||||||
return appendToList(state, ['reactions', action.id], action.accounts, action.next);
|
|
||||||
case REACTIONS_FETCH_REQUEST:
|
|
||||||
case REACTIONS_EXPAND_REQUEST:
|
|
||||||
return state.setIn(['reactions', action.id, 'isLoading'], true);
|
|
||||||
case REACTIONS_FETCH_FAIL:
|
|
||||||
case REACTIONS_EXPAND_FAIL:
|
|
||||||
return state.setIn(['reactions', action.id, 'isLoading'], false);
|
|
||||||
case REBLOGS_FETCH_SUCCESS:
|
case REBLOGS_FETCH_SUCCESS:
|
||||||
return normalizeList(state, ['reblogged_by', action.id], action.accounts, action.next);
|
return normalizeList(state, ['reblogged_by', action.id], action.accounts, action.next);
|
||||||
case REBLOGS_EXPAND_SUCCESS:
|
case REBLOGS_EXPAND_SUCCESS:
|
||||||
|
|
Loading…
Reference in a new issue