Your First Feature: Messaging Friends¶
Let’s dive into implementing a new feature for our social network app. From that we’ll get a better idea of how to build DAML applications using our template.
Right now our app allows us to add friends to our network, but we can’t communicate with them! Let’s fix that by adding a private messaging feature. This should allow a user to send messages to a chosen friend, and see all messages that have been sent to them. Of course we must make sure that no one can see messages that were not sent to them. We will see that DAML lets us implement this in a direct and intuitive way.
There are two parts to building the messaging feature: the DAML code and the UI. Let’s start with adding to the DAML code, on which we will base our UI changes.
DAML Changes¶
The DAML code defines the workflow of the application. This means: what interactions between users (or parties) are permitted by the system? In the context of our feature, the question is: when is a user allowed to message another user?
The approach we’ll take is: a user Bob can message another user Alice if Alice has added Bob as a friend. Remember that friendships are single-directional in our app. So Alice adding Bob as a friend means that she gives permission (or authority) for Bob to send her a message.
In DAML this workflow is represented as a new choice on the User
contract.
nonconsuming choice SendMessage: ContractId Message with
sender: Party
content: Text
controller sender
do
assertMsg "You must be a friend to send a message" (sender `elem` friends)
create Message with sender, receiver = username, content
Let’s break this down.
The choice is nonconsuming
because sending a message should not affect the existence of the User
contract.
By convention, the choice returns the ContractId
of the resulting Message
contract (which we’ll show next).
The parameters to the choice are the sender (the party wishing to talk to the signatory of this User
contract) and the message text.
The controller
clause suggests that it is the sender
who can exercise the choice.
Finally, the body of the choice makes sure that the sender is a friend of the user and then creates the Message
with the sender, receiver and content.
Now let’s see the Message
contract template.
This is very simple - data and no choices - as well as the signatory
declaration.
template Message with
sender: Party
receiver: Party
content: Text
where
signatory sender, receiver
Note that we have two signatories on the Message
contract: both the sender and receiver.
This enforces the fact that the contract creation (and archival) must be authorized by both parties.
Now we’ve specified the workflow of sending messages, let’s integrate the functionality into our app.
TypeScript Code Generation¶
Remember that we interface with our DAML code from the UI components using the generated TypeScript. Since we have changed our DAML code, we also need to rerun the TypeScript code generator. Let’s do this now by running:
daml build
daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts/src
As the TypeScript code is generated into the separate daml-ts
workspace on which the UI depends, we need to rebuild the workspaces from the root directory using:
yarn workspaces run build
We should now have the updated TypeScript code with equivalents of the Message
template and SendMessage
choice.
Now let’s implement our messaging feature in the UI!
Messaging UI¶
Our messaging feature has two parts: a form with inputs for selecting friends and composing the message text, and a “feed” of messages that have been sent to you. Both parts will be implemented as React components that render on the main screen.
MessageList Component¶
The MessageList component is fairly straight-forward: it queries all Message
contracts and displays their contents as a list.
Here is the code for the entire component.
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react'
import { List, ListItem } from 'semantic-ui-react';
import { Message } from '@daml2ts/create-daml-app/lib/create-daml-app-0.1.0/User';
import { useParty, useQuery } from '@daml/react';
/**
* React component displaying the list of messages for the current user.
*/
const MessageList: React.FC = () => {
const username = useParty();
const messagesResult = useQuery(Message, () => ({receiver: username}), []);
const messages = messagesResult.contracts.map(message => message.payload);
const showMessage = (message: Message): string => {
return (message.sender + ": " + message.content);
}
return (
<List relaxed>
{messages.map(message => <ListItem>{showMessage(message)}</ListItem>)}
</List>
);
}
export default MessageList;
The messagesResult
tracks the state of Message
contracts on the ledger, where we specify no restrictions on the query.
We extract the actual message data in messages
.
Note that for any particular user, the Message
query yields exactly the messages that have been either written by or sent to that user.
This is due to how we modelled the signatories and observers in the Message
template, and means we do not risk a privacy breach coming from the application code.
Message Edit Component¶
In addition to the feed component, we need a component for composing messages and sending them using the appropriate choice on the User
contract.
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react'
import { Form, Input, Dropdown, Button } from 'semantic-ui-react';
import { Party } from '@daml/types';
import { User } from '@daml2ts/create-daml-app/lib/create-daml-app-0.1.0/User';
import { useParty, useExerciseByKey } from '@daml/react';
type Props = {
friends: Party[];
}
/**
* React component to edit a message to send to a friend.
*/
const MessageEdit: React.FC<Props> = ({friends}) => {
const sender = useParty();
const [receiver, setReceiver] = React.useState('');
const [content, setContent] = React.useState('');
const [exerciseSendMessage] = useExerciseByKey(User.SendMessage);
const [isSubmitting, setIsSubmitting] = React.useState(false);
const sendMessage = async (receiver: string, content: string): Promise<boolean> => {
try {
await exerciseSendMessage(receiver, {sender, content});
return true;
} catch (error) {
alert("Error sending message:\n" + JSON.stringify(error));
return false;
}
}
const submitMessage = async (event?: React.FormEvent) => {
if (event) {
event.preventDefault();
}
setIsSubmitting(true);
const success = await sendMessage(receiver, content);
setIsSubmitting(false);
if (success) {
// Keep the receiver selected for follow-on messages
// but clear the message text.
setContent('');
}
}
// Options for dropdown menu
const friendOptions = friends.map(f => ({ key: f, text: f, value: f }));
return (
<Form onSubmit={submitMessage}>
<Dropdown
fluid
selection
placeholder='Select friend'
options={friendOptions}
value={receiver}
onChange={(event) => setReceiver(event.currentTarget.textContent ?? '')}
/>
<br />
<Input
fluid
transparent
readOnly={isSubmitting}
loading={isSubmitting}
placeholder="Write a message"
value={content}
onChange={(event) => setContent(event.currentTarget.value)}
/>
<br />
<Button type="submit">Send</Button>
</Form>
);
};
export default MessageEdit;
In this component we use React hooks to track the current choice of message receiver and content.
The exerciseSendMessage
hook gives us a function to exercise the appropriate choice on our User
.
We wrap this in the sendMessage
function to report potential errors to the user, and then the submitMessage
function, called when the “Send” button is clicked.
The isSubmitting
state is used to ensure that message requests are processed one at a time.
The result of each send is a new Message
contract created on the ledger.
View Component¶
The MainView
component composes the different subcomponents (for our friends, the network and the messaging components above) to build the full app view.
We first import our two new components.
import MessageEdit from './MessageEdit';
import MessageList from './MessageList';
Then we can integrate our new messaging components into the main screen view.
In another segment we add the panel including our two new components: the MessageEdit
and the MessageList
.
<Segment>
<Header as='h2'>
<Icon name='pencil square' />
<Header.Content>
Messages
<Header.Subheader>Send a message to a friend</Header.Subheader>
</Header.Content>
</Header>
<MessageEdit
friends={friends.map(user => user.username)}
/>
<Divider />
<MessageList />
</Segment>
This wraps up the implementation of your first end-to-end DAML feature! Let’s give the new functionality a spin. Follow the instructions in “Running the app” to start up the new app.