diff --git a/src/components/Messages/Messages.css b/src/components/Messages/Messages.css
new file mode 100644
index 00000000..c00e5534
--- /dev/null
+++ b/src/components/Messages/Messages.css
@@ -0,0 +1,80 @@
+@import '../../marketplace.css';
+
+.root {
+ margin: 0;
+
+ /* Clearfix */
+ width: 100%;
+ overflow: auto;
+}
+
+.messageItem {
+ margin-bottom: 35px;
+
+ @media (--viewportMedium) {
+ margin-bottom: 40px;
+ }
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+}
+
+.message,
+.ownMessage {
+ /* Preserve newlines, but collapse other whitespace */
+ white-space: pre-line;
+ max-width: 433px;
+}
+
+.message {
+ display: flex;
+ flex-direction: row;
+}
+
+.ownMessage {
+ float: right;
+}
+
+.avatar {
+ flex-shrink: 0;
+ margin: 9px 12px 0 0;
+}
+
+.messageContent,
+.ownMessageContent {
+ @apply --marketplaceMessageFontStyles;
+
+ margin: 0;
+ padding: 15px 14px 11px 14px;
+ border-radius: 8px;
+ box-shadow: var(--boxShadow);
+
+ @media (--viewportMedium) {
+ margin: 0;
+ }
+}
+
+.messageContent {
+ flex: 1;
+}
+
+.ownMessageContent {
+ background-color: var(--marketplaceColor);
+ color: var(--matterColorLight);
+}
+
+.messageDate,
+.ownMessageDate {
+ @apply --marketplaceMessageDateFontStyles;
+ margin: 11px 0 0 0;
+ color: var(--matterColorAnti);
+
+ @media (--viewportMedium) {
+ margin: 14px 0 0 0;
+ }
+}
+
+.ownMessageDate {
+ text-align: right;
+}
diff --git a/src/components/Messages/Messages.example.js b/src/components/Messages/Messages.example.js
new file mode 100644
index 00000000..69fed49e
--- /dev/null
+++ b/src/components/Messages/Messages.example.js
@@ -0,0 +1,35 @@
+import { createUser, createCurrentUser, createMessage } from '../../util/test-data';
+import Messages from './Messages';
+
+export const Empty = {
+ component: Messages,
+ props: {
+ messages: [],
+ currentUser: null,
+ },
+ group: 'messages',
+};
+
+export const WithoutCurrentUser = {
+ component: Messages,
+ props: {
+ messages: [
+ createMessage('msg1', {}, { sender: createUser('user1') }),
+ createMessage('msg2', {}, { sender: createUser('user2') }),
+ ],
+ currentUser: null,
+ },
+ group: 'messages',
+};
+
+export const WithCurrentUser = {
+ component: Messages,
+ props: {
+ messages: [
+ createMessage('msg1', {}, { sender: createUser('user1') }),
+ createMessage('msg2', {}, { sender: createUser('user2') }),
+ ],
+ currentUser: createCurrentUser('user2'),
+ },
+ group: 'messages',
+};
diff --git a/src/components/Messages/Messages.js b/src/components/Messages/Messages.js
new file mode 100644
index 00000000..cf622cba
--- /dev/null
+++ b/src/components/Messages/Messages.js
@@ -0,0 +1,92 @@
+import React from 'react';
+import { string, arrayOf } from 'prop-types';
+import { injectIntl, intlShape } from 'react-intl';
+import classNames from 'classnames';
+import { Avatar } from '../../components';
+import { formatDate } from '../../util/dates';
+import * as propTypes from '../../util/propTypes';
+
+import css from './Messages.css';
+
+const Message = props => {
+ const { message, intl } = props;
+ const todayString = intl.formatMessage({ id: 'Messages.today' });
+ return (
+
+
+
+
{message.attributes.content}
+
{formatDate(intl, todayString, message.attributes.at)}
+
+
+ );
+};
+
+Message.propTypes = {
+ message: propTypes.message.isRequired,
+ intl: intlShape.isRequired,
+};
+
+const OwnMessage = props => {
+ const { message, intl } = props;
+ const todayString = intl.formatMessage({ id: 'Messages.today' });
+ return (
+
+
{message.attributes.content}
+
{formatDate(intl, todayString, message.attributes.at)}
+
+ );
+};
+
+OwnMessage.propTypes = {
+ message: propTypes.message.isRequired,
+ intl: intlShape.isRequired,
+};
+
+export const MessagesComponent = props => {
+ const { rootClassName, className, messages, currentUser, intl } = props;
+ const classes = classNames(rootClassName || css.root, className);
+
+ const msg = message => {
+ const isOwnMessage =
+ message.sender &&
+ message.sender.id &&
+ currentUser &&
+ currentUser.id &&
+ message.sender.id.uuid === currentUser.id.uuid;
+ if (isOwnMessage) {
+ return ;
+ }
+ return ;
+ };
+
+ return (
+
+ {messages.map(m => (
+ -
+ {msg(m)}
+
+ ))}
+
+ );
+};
+
+MessagesComponent.defaultProps = {
+ rootClassName: null,
+ className: null,
+};
+
+MessagesComponent.propTypes = {
+ rootClassName: string,
+ className: string,
+
+ messages: arrayOf(propTypes.message),
+ currentUser: propTypes.currentUser,
+
+ // from injectIntl
+ intl: intlShape.isRequired,
+};
+
+const Messages = injectIntl(MessagesComponent);
+
+export default Messages;
diff --git a/src/components/Messages/Messages.test.js b/src/components/Messages/Messages.test.js
new file mode 100644
index 00000000..d8ee9f6a
--- /dev/null
+++ b/src/components/Messages/Messages.test.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { renderDeep } from '../../util/test-helpers';
+import { fakeIntl, createUser, createCurrentUser, createMessage } from '../../util/test-data';
+import { MessagesComponent } from './Messages';
+
+describe('Messages', () => {
+ it('matches snapshot', () => {
+ const props = {
+ messages: [
+ createMessage('msg1', {}, { sender: createUser('user1') }),
+ createMessage('msg2', {}, { sender: createUser('user2') }),
+ ],
+ currentUser: createCurrentUser('user2'),
+ intl: fakeIntl,
+ };
+ const tree = renderDeep();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/src/components/Messages/__snapshots__/Messages.test.js.snap b/src/components/Messages/__snapshots__/Messages.test.js.snap
new file mode 100644
index 00000000..b054976d
--- /dev/null
+++ b/src/components/Messages/__snapshots__/Messages.test.js.snap
@@ -0,0 +1,63 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Messages matches snapshot 1`] = `
+
+ -
+
+
+
+ TT
+
+
+
+
+ Message msg1
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+ 2017-11-09, 08:12
+
+
+
+
+ -
+
+
+ Message msg2
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+ 2017-11-09, 08:12
+
+
+
+
+`;
diff --git a/src/components/index.js b/src/components/index.js
index df575f41..20493614 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -49,10 +49,10 @@ export { default as ImageCarousel } from './ImageCarousel/ImageCarousel';
export { default as ImageFromFile } from './ImageFromFile/ImageFromFile';
export { default as LayoutSideNavigation } from './LayoutSideNavigation/LayoutSideNavigation';
export { default as LayoutSingleColumn } from './LayoutSingleColumn/LayoutSingleColumn';
-export { default as LayoutWrapperTopbar } from './LayoutWrapperTopbar/LayoutWrapperTopbar';
+export { default as LayoutWrapperFooter } from './LayoutWrapperFooter/LayoutWrapperFooter';
export { default as LayoutWrapperMain } from './LayoutWrapperMain/LayoutWrapperMain';
export { default as LayoutWrapperSideNav } from './LayoutWrapperSideNav/LayoutWrapperSideNav';
-export { default as LayoutWrapperFooter } from './LayoutWrapperFooter/LayoutWrapperFooter';
+export { default as LayoutWrapperTopbar } from './LayoutWrapperTopbar/LayoutWrapperTopbar';
export { default as ListingCard } from './ListingCard/ListingCard';
export {
default as LocationAutocompleteInput,
@@ -66,6 +66,7 @@ export { default as Menu } from './Menu/Menu';
export { default as MenuContent } from './MenuContent/MenuContent';
export { default as MenuItem } from './MenuItem/MenuItem';
export { default as MenuLabel } from './MenuLabel/MenuLabel';
+export { default as Messages } from './Messages/Messages';
export { default as Modal } from './Modal/Modal';
export { default as ModalInMobile } from './ModalInMobile/ModalInMobile';
export { default as NamedLink } from './NamedLink/NamedLink';
diff --git a/src/examples.js b/src/examples.js
index 6209d7dc..00e62fda 100644
--- a/src/examples.js
+++ b/src/examples.js
@@ -29,6 +29,7 @@ import * as LocationAutocompleteInput from './components/LocationAutocompleteInp
import * as ManageListingCard from './components/ManageListingCard/ManageListingCard.example';
import * as Map from './components/Map/Map.example';
import * as Menu from './components/Menu/Menu.example';
+import * as Messages from './components/Messages/Messages.example';
import * as Modal from './components/Modal/Modal.example';
import * as ModalInMobile from './components/ModalInMobile/ModalInMobile.example';
import * as NamedLink from './components/NamedLink/NamedLink.example';
@@ -97,6 +98,7 @@ export {
ManageListingCard,
Map,
Menu,
+ Messages,
Modal,
ModalInMobile,
NamedLink,
diff --git a/src/marketplaceFonts.css b/src/marketplaceFonts.css
index 68cd7df2..5ef40a57 100644
--- a/src/marketplaceFonts.css
+++ b/src/marketplaceFonts.css
@@ -210,6 +210,27 @@
}
}
+ --marketplaceMessageFontStyles {
+ font-family: 'sofiapro', Helvetica, Arial, sans-serif;
+ font-weight: var(--fontWeightRegular);
+ font-size: 16px;
+ line-height: 24px;
+ }
+
+ --marketplaceMessageDateFontStyles {
+ font-family: 'sofiapro', Helvetica, Arial, sans-serif;
+ font-weight: var(--fontWeightRegular);
+ font-size: 13px;
+ line-height: 18px;
+ letter-spacing: -0.1px;
+
+ @media (--viewportMedium) {
+ font-weight: var(--fontWeightMedium);
+ font-size: 14px;
+ line-height: 24px;
+ }
+ }
+
/* ================ Tabbed navigation font styles ================ */
--marketplaceTabNavFontStyles {
diff --git a/src/translations/en.json b/src/translations/en.json
index 19f76899..8a95a55d 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -248,6 +248,7 @@
"ManageListingsPage.youHaveListings": "You have {count} {count, plural, one {listing} other {listings}}",
"ManageListingsPage.yourListings": "Your listings",
"MapPriceMarker.unsupportedPrice": "({currency})",
+ "Messages.today": "Today",
"Modal.close": "CLOSE",
"Modal.closeModal": "Close modal",
"NotFoundPage.description": "We can't find the page or the sauna you're looking for. Make sure you've typed the URL correctly, of try searching Saunatime.",
diff --git a/src/util/propTypes.js b/src/util/propTypes.js
index 1935415d..b983b275 100644
--- a/src/util/propTypes.js
+++ b/src/util/propTypes.js
@@ -62,6 +62,22 @@ export const place = shape({
country: string, // country code, e.g. FI, US
});
+// Denormalised image object
+export const image = shape({
+ id: uuid.isRequired,
+ type: value('image').isRequired,
+ attributes: shape({
+ sizes: arrayOf(
+ shape({
+ width: number.isRequired,
+ height: number.isRequired,
+ name: string.isRequired,
+ url: string.isRequired,
+ })
+ ).isRequired,
+ }),
+});
+
// Denormalised user object
export const currentUser = shape({
id: uuid.isRequired,
@@ -79,6 +95,7 @@ export const currentUser = shape({
}).isRequired,
stripeConnected: bool.isRequired,
}),
+ profileImage: image,
});
// Denormalised user object
@@ -93,22 +110,7 @@ export const user = shape({
bio: string,
}),
}),
-});
-
-// Denormalised image object
-export const image = shape({
- id: uuid.isRequired,
- type: value('image').isRequired,
- attributes: shape({
- sizes: arrayOf(
- shape({
- width: number.isRequired,
- height: number.isRequired,
- name: string.isRequired,
- url: string.isRequired,
- })
- ).isRequired,
- }),
+ profileImage: image,
});
// Denormalised listing object
@@ -212,6 +214,17 @@ export const transaction = shape({
provider: user,
});
+// Denormalised transaction message
+export const message = shape({
+ id: uuid.isRequired,
+ type: value('message').isRequired,
+ attributes: shape({
+ at: instanceOf(Date).isRequired,
+ content: string.isRequired,
+ }).isRequired,
+ sender: user,
+});
+
// Pagination information in the response meta
export const pagination = shape({
page: number.isRequired,
diff --git a/src/util/test-data.js b/src/util/test-data.js
index 5c682b38..5c329e3f 100644
--- a/src/util/test-data.js
+++ b/src/util/test-data.js
@@ -131,6 +131,19 @@ export const createTransaction = options => {
};
};
+export const createMessage = (id, attributes = {}, includes = {}) => {
+ return {
+ id: new UUID(id),
+ type: 'message',
+ attributes: {
+ at: new Date(Date.UTC(2017, 10, 9, 8, 12)),
+ content: `Message ${id}\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`,
+ ...attributes,
+ },
+ ...includes,
+ };
+};
+
// Default config for currency formatting in tests and examples.
export const currencyConfig = {
style: 'currency',