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:
Essem 2024-01-27 00:22:11 -06:00
parent a4bfa67151
commit 31406cb0f2
No known key found for this signature in database
GPG key ID: 7D497397CC3A2A8C
9 changed files with 138 additions and 42 deletions

View file

@ -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,
}; };
} }

View file

@ -0,0 +1,5 @@
export interface ApiStatusReactionJSON {
name: string;
static_url?: string | undefined;
url?: string | undefined;
}

View file

@ -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'>

View file

@ -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>
); );
}; };

View file

@ -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>

View 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: '',
});

View file

@ -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,
}; };

View 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;
}
}

View file

@ -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: