mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 03:43:28 +10:00
454 lines
14 KiB
JavaScript
454 lines
14 KiB
JavaScript
import React from 'react';
|
|
import { string, arrayOf, bool, func, number } from 'prop-types';
|
|
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
|
|
import { dropWhile } from 'lodash';
|
|
import classNames from 'classnames';
|
|
import { Avatar, InlineTextButton, ReviewRating } from '../../components';
|
|
import { formatDate } from '../../util/dates';
|
|
import { ensureTransaction, ensureUser, ensureListing, userDisplayName } from '../../util/data';
|
|
import * as propTypes from '../../util/propTypes';
|
|
import * as log from '../../util/log';
|
|
|
|
import css from './ActivityFeed.css';
|
|
|
|
const Message = props => {
|
|
const { message, intl } = props;
|
|
const todayString = intl.formatMessage({ id: 'ActivityFeed.today' });
|
|
return (
|
|
<div className={css.message}>
|
|
<Avatar className={css.avatar} user={message.sender} />
|
|
<div>
|
|
<p className={css.messageContent}>{message.attributes.content}</p>
|
|
<p className={css.messageDate}>{formatDate(intl, todayString, message.attributes.at)}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
Message.propTypes = {
|
|
message: propTypes.message.isRequired,
|
|
intl: intlShape.isRequired,
|
|
};
|
|
|
|
const OwnMessage = props => {
|
|
const { message, intl } = props;
|
|
const todayString = intl.formatMessage({ id: 'ActivityFeed.today' });
|
|
return (
|
|
<div className={css.ownMessage}>
|
|
<div className={css.ownMessageContentWrapper}>
|
|
<p className={css.ownMessageContent}>{message.attributes.content}</p>
|
|
</div>
|
|
<p className={css.ownMessageDate}>{formatDate(intl, todayString, message.attributes.at)}</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
OwnMessage.propTypes = {
|
|
message: propTypes.message.isRequired,
|
|
intl: intlShape.isRequired,
|
|
};
|
|
|
|
const Review = props => {
|
|
const { content, rating } = props;
|
|
return (
|
|
<div>
|
|
<p className={css.reviewContent}>{content}</p>
|
|
<ReviewRating
|
|
reviewStarClassName={css.reviewStar}
|
|
className={css.reviewStars}
|
|
rating={rating}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
Review.propTypes = {
|
|
content: string.isRequired,
|
|
rating: number.isRequired,
|
|
};
|
|
|
|
// Check if a transition is the kind that
|
|
// should be rendered in he ActivityFeed
|
|
const shouldRenderTransition = transition => {
|
|
return [
|
|
propTypes.TX_TRANSITION_PREAUTHORIZE,
|
|
propTypes.TX_TRANSITION_PREAUTHORIZE_ENQUIRY,
|
|
propTypes.TX_TRANSITION_ACCEPT,
|
|
propTypes.TX_TRANSITION_DECLINE,
|
|
propTypes.TX_TRANSITION_AUTO_DECLINE,
|
|
propTypes.TX_TRANSITION_CANCEL,
|
|
propTypes.TX_TRANSITION_MARK_DELIVERED,
|
|
propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST,
|
|
propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND,
|
|
propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST,
|
|
propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND,
|
|
].includes(transition);
|
|
};
|
|
|
|
// Check if a user giving a review is related to
|
|
// given tx transition.
|
|
const isReviewTransition = transition => {
|
|
return [
|
|
propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST,
|
|
propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST,
|
|
propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND,
|
|
propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND,
|
|
].includes(transition);
|
|
};
|
|
|
|
const hasUserLeftAReviewFirst = (userRole, lastTransition) => {
|
|
return (
|
|
(lastTransition === propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST &&
|
|
userRole === propTypes.TX_TRANSITION_ACTOR_CUSTOMER) ||
|
|
(lastTransition === propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST &&
|
|
userRole === propTypes.TX_TRANSITION_ACTOR_PROVIDER) ||
|
|
propTypes.areReviewsCompleted(lastTransition)
|
|
);
|
|
};
|
|
|
|
const resolveTransitionMessage = (
|
|
transition,
|
|
lastTransition,
|
|
listingTitle,
|
|
ownRole,
|
|
otherUsersName,
|
|
intl,
|
|
onOpenReviewModal
|
|
) => {
|
|
const isOwnTransition = transition.by === ownRole;
|
|
const currentTransition = transition.transition;
|
|
const displayName = otherUsersName;
|
|
const deliveredState = lastTransition === propTypes.TX_TRANSITION_MARK_DELIVERED;
|
|
|
|
switch (currentTransition) {
|
|
case propTypes.TX_TRANSITION_PREAUTHORIZE:
|
|
case propTypes.TX_TRANSITION_PREAUTHORIZE_ENQUIRY:
|
|
return isOwnTransition ? (
|
|
<FormattedMessage id="ActivityFeed.ownTransitionRequest" values={{ listingTitle }} />
|
|
) : (
|
|
<FormattedMessage
|
|
id="ActivityFeed.transitionRequest"
|
|
values={{ displayName, listingTitle }}
|
|
/>
|
|
);
|
|
case propTypes.TX_TRANSITION_ACCEPT:
|
|
return isOwnTransition ? (
|
|
<FormattedMessage id="ActivityFeed.ownTransitionAccept" />
|
|
) : (
|
|
<FormattedMessage id="ActivityFeed.transitionAccept" values={{ displayName }} />
|
|
);
|
|
case propTypes.TX_TRANSITION_DECLINE:
|
|
return isOwnTransition ? (
|
|
<FormattedMessage id="ActivityFeed.ownTransitionDecline" />
|
|
) : (
|
|
<FormattedMessage id="ActivityFeed.transitionDecline" />
|
|
);
|
|
case propTypes.TX_TRANSITION_AUTO_DECLINE:
|
|
return ownRole === propTypes.TX_TRANSITION_ACTOR_PROVIDER ? (
|
|
<FormattedMessage id="ActivityFeed.ownTransitionAutoDecline" />
|
|
) : (
|
|
<FormattedMessage id="ActivityFeed.transitionAutoDecline" values={{ displayName }} />
|
|
);
|
|
case propTypes.TX_TRANSITION_CANCEL:
|
|
return <FormattedMessage id="ActivityFeed.transitionCancel" />;
|
|
case propTypes.TX_TRANSITION_MARK_DELIVERED:
|
|
// Show the leave a review link if the state is delivered or
|
|
// if current user is not the first to leave a review
|
|
const reviewLink =
|
|
deliveredState || !hasUserLeftAReviewFirst(ownRole, lastTransition) ? (
|
|
<InlineTextButton onClick={onOpenReviewModal}>
|
|
<FormattedMessage id="ActivityFeed.leaveAReview" values={{ displayName }} />
|
|
</InlineTextButton>
|
|
) : null;
|
|
|
|
return <FormattedMessage id="ActivityFeed.transitionComplete" values={{ reviewLink }} />;
|
|
case propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST:
|
|
case propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST:
|
|
if (isOwnTransition) {
|
|
return <FormattedMessage id="ActivityFeed.ownTransitionReview" values={{ displayName }} />;
|
|
} else {
|
|
// show the leave a review link if current user is not the first
|
|
// one to leave a review
|
|
const reviewLink = !hasUserLeftAReviewFirst(ownRole, lastTransition) ? (
|
|
<InlineTextButton onClick={onOpenReviewModal}>
|
|
<FormattedMessage id="ActivityFeed.leaveAReviewSecond" values={{ displayName }} />
|
|
</InlineTextButton>
|
|
) : null;
|
|
return (
|
|
<FormattedMessage
|
|
id="ActivityFeed.transitionReview"
|
|
values={{ displayName, reviewLink }}
|
|
/>
|
|
);
|
|
}
|
|
case propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND:
|
|
case propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND:
|
|
if (isOwnTransition) {
|
|
return <FormattedMessage id="ActivityFeed.ownTransitionReview" values={{ displayName }} />;
|
|
} else {
|
|
return (
|
|
<FormattedMessage
|
|
id="ActivityFeed.transitionReview"
|
|
values={{ displayName, reviewLink: null }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
default:
|
|
log.error(new Error('Unknown transaction transition type'), 'unknown-transition-type', {
|
|
transitionType: currentTransition,
|
|
});
|
|
return '';
|
|
}
|
|
};
|
|
|
|
const reviewByAuthorId = (transaction, userId) => {
|
|
return transaction.reviews.filter(r => r.author.id.uuid === userId.uuid)[0];
|
|
};
|
|
|
|
const Transition = props => {
|
|
const { transition, transaction, currentUser, intl, onOpenReviewModal } = props;
|
|
|
|
const currentTransaction = ensureTransaction(transaction);
|
|
const customer = currentTransaction.customer;
|
|
const provider = currentTransaction.provider;
|
|
|
|
const deletedListing = intl.formatMessage({
|
|
id: 'ActivityFeed.deletedListing',
|
|
});
|
|
const listingTitle = currentTransaction.listing.attributes.deleted
|
|
? deletedListing
|
|
: currentTransaction.listing.attributes.title;
|
|
const lastTransition = currentTransaction.attributes.lastTransition;
|
|
|
|
const ownRole =
|
|
currentUser.id.uuid === customer.id.uuid
|
|
? propTypes.TX_TRANSITION_ACTOR_CUSTOMER
|
|
: propTypes.TX_TRANSITION_ACTOR_PROVIDER;
|
|
|
|
const bannedUserDisplayName = intl.formatMessage({
|
|
id: 'ActivityFeed.bannedUserDisplayName',
|
|
});
|
|
const otherUsersName =
|
|
ownRole === propTypes.TX_TRANSITION_ACTOR_CUSTOMER
|
|
? userDisplayName(provider, bannedUserDisplayName)
|
|
: userDisplayName(customer, bannedUserDisplayName);
|
|
|
|
const transitionMessage = resolveTransitionMessage(
|
|
transition,
|
|
lastTransition,
|
|
listingTitle,
|
|
ownRole,
|
|
otherUsersName,
|
|
intl,
|
|
onOpenReviewModal
|
|
);
|
|
const currentTransition = transition.transition;
|
|
|
|
let reviewComponent = null;
|
|
|
|
if (isReviewTransition(currentTransition) && propTypes.areReviewsCompleted(lastTransition)) {
|
|
const customerReview =
|
|
currentTransition === propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST ||
|
|
currentTransition === propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND;
|
|
const providerReview =
|
|
currentTransition === propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST ||
|
|
currentTransition === propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND;
|
|
if (customerReview) {
|
|
const review = reviewByAuthorId(currentTransaction, customer.id);
|
|
reviewComponent = (
|
|
<Review content={review.attributes.content} rating={review.attributes.rating} />
|
|
);
|
|
} else if (providerReview) {
|
|
const review = reviewByAuthorId(currentTransaction, provider.id);
|
|
reviewComponent = (
|
|
<Review content={review.attributes.content} rating={review.attributes.rating} />
|
|
);
|
|
}
|
|
}
|
|
|
|
const todayString = intl.formatMessage({ id: 'ActivityFeed.today' });
|
|
|
|
return (
|
|
<div className={css.transition}>
|
|
<div className={css.bullet}>
|
|
<p className={css.transitionContent}>•</p>
|
|
</div>
|
|
<div>
|
|
<p className={css.transitionContent}>{transitionMessage}</p>
|
|
<p className={css.transitionDate}>{formatDate(intl, todayString, transition.at)}</p>
|
|
{reviewComponent}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
Transition.propTypes = {
|
|
transition: propTypes.txTransition.isRequired,
|
|
transaction: propTypes.transaction.isRequired,
|
|
currentUser: propTypes.currentUser.isRequired,
|
|
intl: intlShape.isRequired,
|
|
onOpenReviewModal: func.isRequired,
|
|
};
|
|
|
|
const EmptyTransition = () => {
|
|
return (
|
|
<div className={css.transition}>
|
|
<div className={css.bullet}>
|
|
<p className={css.transitionContent}>•</p>
|
|
</div>
|
|
<div>
|
|
<p className={css.transitionContent} />
|
|
<p className={css.transitionDate} />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const isMessage = item => item && item.type === 'message';
|
|
|
|
// Compare function for sorting an array containint messages and transitions
|
|
const compareItems = (a, b) => {
|
|
const itemDate = item => (isMessage(item) ? item.attributes.at : item.at);
|
|
return itemDate(a) - itemDate(b);
|
|
};
|
|
|
|
const organizedItems = (messages, transitions, hideOldTransitions) => {
|
|
const items = messages.concat(transitions).sort(compareItems);
|
|
if (hideOldTransitions) {
|
|
// Hide transitions that happened before the oldest message. Since
|
|
// we have older items (messages) that we are not showing, seeing
|
|
// old transitions would be confusing.
|
|
return dropWhile(items, i => !isMessage(i));
|
|
} else {
|
|
return items;
|
|
}
|
|
};
|
|
|
|
export const ActivityFeedComponent = props => {
|
|
const {
|
|
rootClassName,
|
|
className,
|
|
messages,
|
|
transaction,
|
|
currentUser,
|
|
hasOlderMessages,
|
|
onOpenReviewModal,
|
|
onShowOlderMessages,
|
|
fetchMessagesInProgress,
|
|
intl,
|
|
} = props;
|
|
const classes = classNames(rootClassName || css.root, className);
|
|
|
|
const currentTransaction = ensureTransaction(transaction);
|
|
const transitions = currentTransaction.attributes.transitions
|
|
? currentTransaction.attributes.transitions
|
|
: [];
|
|
const currentCustomer = ensureUser(currentTransaction.customer);
|
|
const currentProvider = ensureUser(currentTransaction.provider);
|
|
const currentListing = ensureListing(currentTransaction.listing);
|
|
|
|
const transitionsAvailable = !!(
|
|
currentUser &&
|
|
currentUser.id &&
|
|
currentCustomer.id &&
|
|
currentProvider.id &&
|
|
currentListing.id
|
|
);
|
|
|
|
// combine messages and transaction transitions
|
|
const items = organizedItems(messages, transitions, hasOlderMessages || fetchMessagesInProgress);
|
|
|
|
const transitionComponent = transition => {
|
|
if (transitionsAvailable) {
|
|
return (
|
|
<Transition
|
|
transition={transition}
|
|
transaction={transaction}
|
|
currentUser={currentUser}
|
|
intl={intl}
|
|
onOpenReviewModal={onOpenReviewModal}
|
|
/>
|
|
);
|
|
} else {
|
|
return <EmptyTransition />;
|
|
}
|
|
};
|
|
|
|
const messageComponent = message => {
|
|
const isOwnMessage =
|
|
message.sender &&
|
|
message.sender.id &&
|
|
currentUser &&
|
|
currentUser.id &&
|
|
message.sender.id.uuid === currentUser.id.uuid;
|
|
if (isOwnMessage) {
|
|
return <OwnMessage message={message} intl={intl} />;
|
|
}
|
|
return <Message message={message} intl={intl} />;
|
|
};
|
|
|
|
const messageListItem = message => {
|
|
return (
|
|
<li id={`msg-${message.id.uuid}`} key={message.id.uuid} className={css.messageItem}>
|
|
{messageComponent(message)}
|
|
</li>
|
|
);
|
|
};
|
|
|
|
const transitionListItem = transition => {
|
|
if (shouldRenderTransition(transition.transition)) {
|
|
return (
|
|
<li key={transition.transition} className={css.transitionItem}>
|
|
{transitionComponent(transition)}
|
|
</li>
|
|
);
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ul className={classes}>
|
|
{hasOlderMessages ? (
|
|
<li className={css.showOlderWrapper} key="show-older-messages">
|
|
<InlineTextButton className={css.showOlderButton} onClick={onShowOlderMessages}>
|
|
<FormattedMessage id="ActivityFeed.showOlderMessages" />
|
|
</InlineTextButton>
|
|
</li>
|
|
) : null}
|
|
{items.map(item => {
|
|
if (isMessage(item)) {
|
|
return messageListItem(item);
|
|
} else {
|
|
return transitionListItem(item);
|
|
}
|
|
})}
|
|
</ul>
|
|
);
|
|
};
|
|
|
|
ActivityFeedComponent.defaultProps = {
|
|
rootClassName: null,
|
|
className: null,
|
|
};
|
|
|
|
ActivityFeedComponent.propTypes = {
|
|
rootClassName: string,
|
|
className: string,
|
|
|
|
currentUser: propTypes.currentUser,
|
|
transaction: propTypes.transaction,
|
|
messages: arrayOf(propTypes.message),
|
|
hasOlderMessages: bool.isRequired,
|
|
onOpenReviewModal: func.isRequired,
|
|
onShowOlderMessages: func.isRequired,
|
|
fetchMessagesInProgress: bool.isRequired,
|
|
|
|
// from injectIntl
|
|
intl: intlShape.isRequired,
|
|
};
|
|
|
|
const ActivityFeed = injectIntl(ActivityFeedComponent);
|
|
|
|
export default ActivityFeed;
|