Message annotations
Message annotations enable clients to append information to existing messages on a channel. You can use annotations to implement features like:
- Message reactions - add emoji reactions (👍, ❤️, 😂) to messages
- Content categorization - tag messages with categories such as "important" or "urgent"
- Community moderation - flag inappropriate content for review
- Read receipts - mark messages as "read" or "delivered"
- Comments - clients can comment on a message, which will not show up in normal channel history, but which can be loaded on demand
When clients publish or delete an annotation, Ably automatically creates a summary that provides an aggregated view of all annotations for the associated message.
Enable annotations
Annotations can be enabled for a channel or channel namespace with the Message annotations, updates, and deletes channel rule.
- Go to the Settings tab of an app in your dashboard.
- Under channel rules, click Add new rule.
- Enter the channel name or channel namespace on which to enable message annotations.
- Check Message annotations, updates, and deletes to enable message annotations.
- Click Create channel rule to save.
Annotation types
Annotation types determine how annotations are processed and aggregated into summaries.
The annotation type is a string of the format namespace:summarization.version where:
namespaceis a string (e.g.reactions) that groups related annotations. Only annotations in the same namespace will be aggregated to produce summaries.summarizationspecifies how annotations are aggregated to produce summaries, such astotal,flag,distinct,unique, ormultiple.versionspecifies the version component which allows for future changes to summarization behavior.
Total
The total.v1 summarization method counts the number of annotations of a given type that were published for a message.
Deleting an annotation decrements the total count for that message.
Using total.v1 does not attribute counts to individual clients in the summary; it maintains only a simple count per annotation type and does not organize counts by name. Unidentified clients can publish total.v1 annotations. Use the identified channel rule if you want to prevent unidentified clients from publishing annotations.
If the same client publishes an annotation of a given type to the same message twice, the total count is incremented twice.
1
2
3
4
5
{
"metrics:total.v1": {
"total": 42
}
}Flag
The flag.v1 summarization method counts how many distinct clients have published an annotation of a given type and maintains a list of those clientIds. Clients must be identified to publish flag.v1 annotations.
Deleting an annotation decrements the total count for that message and removes the clientId from the list of clients that contributed to the summary.
A given client can contribute to the summary only once per annotation type.
1
2
3
4
5
6
7
{
"reactions:flag.v1": {
"total": 3,
"clientIds": ["client1", "client2", "client3"],
"clipped": false
}
}Distinct
The distinct.v1 summarization method counts how many unique clients have published an annotation with a given name for each annotation type along with the corresponding list of clientIds that published it. Clients must be identified to publish distinct.v1 annotations.
A given client can contribute to the summary for a particular annotation name only once, but the same client may publish additional annotations with different names.
Deleting an annotation removes the clientId from the list of clients that contributed to the summary for that name, and decrements the total count for that name.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"categories:distinct.v1": {
"important": {
"total": 2,
"clientIds": ["client1", "client3"],
"clipped": false
},
"urgent": {
"total": 3,
"clientIds": ["client1", "client2", "client3"],
"clipped": false
}
}
}Unique
The unique.v1 summarization method counts how many unique clients have published an annotation with a given name for each annotation type, while guaranteeing that each client contributes to the summary for only one name at a time. The summary for each annotation name holds a total count of the number of distinct clients that have published an annotation with that name along with the corresponding list of clientIds that published it. Clients must be identified to publish unique.v1 annotations.
A given client can contribute to the summary for a particular annotation name only once. Publishing an annotation with a different name automatically removes that client from the summary for the previous name and adds them to the new one, updating the affected total values and list of clientIds.
Deleting an annotation removes the clientId from the list of clients that contributed to the summary for that name, and decrements the total count for that name.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"status:unique.v1": {
"important": {
"total": 2,
"clientIds": ["client1", "client3"],
"clipped": false
},
"urgent": {
"total": 1,
"clientIds": ["client2"],
"clipped": false
}
}
}Multiple
The multiple.v1 summarization method counts both a total and a per-client count of the number of annotations that were published with a given name for each annotation type. Additionally it includes a count for the number of annotations published with a given name from unidentified clients. Use the identified channel rule if you want to prevent unidentified clients from publishing annotations.
A given client can contribute to the summary for a particular annotation name multiple times. The same client may also publish additional annotations with different names.
If a client specifies a count when publishing an annotation, the client's contribution to the summary is incremented by the specified value. If not, it defaults to incrementing by 1.
Deleting an annotation removes all contributions made by that clientId for that name.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"voting:multiple.v1": {
"option-a": {
"total": 7,
"clientCounts": {
"client1": 3,
"client2": 2
},
"totalUnidentified": 2,
"clipped": false,
"totalClientIds": 2
},
"option-b": {
"total": 4,
"clientCounts": {
"client1": 2,
"client3": 1
},
"totalUnidentified": 1,
"clipped": false,
"totalClientIds": 2
}
}
}Publish annotations
To publish an annotation for a message, use the annotations.publish() method on a channel. Pass in either a message instance or the serial of the message to annotate. This method will publish an annotation with the action annotation.create.
Certain annotation summarization methods require a client to be identified for them to be able to publish an annotation. Their clientId will then be included in the associated published annotation.
Specify the annotation type using the type field of the annotation object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Create an Ably Realtime client specifying the clientId that will
// be associated with annotations published by this client
const realtime = new Ably.Realtime({ key: 'demokey:*****', clientId: 'my-client-id' });
// Create a channel in a namespace called `annotations`
// which has message annotations enabled
const channel = realtime.channels.get('annotations:example');
// Publish an annotation for a message that flags it as delivered
await channel.annotations.publish(message, {
type: 'receipts:flag.v1',
name: 'delivered'
});
// You can also use a message's `serial`
await channel.annotations.publish(message.serial, {
type: 'receipts:flag.v1',
name: 'delivered'
});In the case of the distinct, unique, or multiple aggregation types, you should also specify a name. For these types, each different name will be aggregated separately in the annotation summary.
In the case of the multiple aggregation type, you should specify both a name and a count, by which to increment a client's contribution to the summary.
1
2
3
4
5
await channel.annotations.publish(message.serial, {
type: 'rating:multiple.v1',
name: 'stars',
count: 4
});You can additionally specify a data payload when publishing an annotation. This is not included in an annotation summary, so only readable by clients that are subscribed to individual annotation events.
Delete annotations
To delete an annotation, use the annotations.delete() method on a channel. Pass in either a message instance or the serial of the message to annotate. This method will publish an annotation message with an action of annotation.delete.
Deleting an annotation does not remove the original annotation that was published. Instead, the deletion modifies the annotation summary for that message by removing the contribution specified by the annotation.
The clientId specified in the client options will be associated with the published delete annotation.
Specify the annotation type using the type field of the annotation object, and optionally specify a name for the annotation. The name is used to aggregate certain annotations when producing an annotation summary.
1
2
3
4
5
6
7
8
9
10
11
12
13
// Create an Ably Realtime client specifying the clientId that will
// be associated with annotations published by this client
const realtime = new Ably.Realtime({ key: 'demokey:*****', clientId: 'my-client-id' });
// Create a channel in a namespace called `annotations`
// which has message annotations enabled
const channel = realtime.channels.get('annotations:example');
// Delete a 'delivered' annotation
await channel.annotations.delete(message.serial, {
type: 'receipts:flag.v1',
name: 'delivered'
});Subscribe to annotation summaries
The simplest way to receive annotation updates is via annotation summaries. A message that has annotations also includes a summary which is a synopsis of certain details of all annotations for that message. Summaries are updated in response to further annotation events for that message, and summary changes are delivered by default to subscribing clients.
Annotation summaries are delivered to subscribers as messages with an action of message.summary, and a serial matching the serial of the message that they are updating. They have an annotations field which contains a summary of all the annotations for the message.
The value of that summary field is an object where the keys are the annotation types. The structure of the value of each key depends on the summarization method used, for example total.v1 will have a total field, while flag.v1 will have total and clientIds fields.
1
2
3
4
5
6
7
8
9
10
11
12
13
// Create an Ably Realtime client specifying the clientId that will
// be associated with annotations published by this client
const realtime = new Ably.Realtime({ key: 'demokey:*****', clientId: 'my-client-id' });
// Create a channel in a namespace called `annotations`
// which has message annotations enabled
const channel = realtime.channels.get('annotations:example');
await channel.subscribe((message) => {
if (message.action === 'message.summary') {
console.log(message.annotations.summary);
}
});Annotation summaries
When annotations for a message are published, Ably automatically generates a summary that provides an aggregated view of all annotations for that message.
A separate summary is produced for each distinct annotation type. The summarization method specified in the annotation type determines how annotations in the same namespace for a given message are aggregated into a summary. A summary is constructed from the set of individual annotation events (annotation messages with an action of annotation.create or annotation.delete).
The summary will be included in a summary field nested within the message's annotations field, and is an object whose keys are the annotation types and whose values describe the annotation summary for that type. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
{
"metrics:total.v1": {
"total": 42
},
"reactions:flag.v1": {
"total": 3,
"clientIds": ["client1", "client2", "client3"],
"clipped": false
},
"categories:distinct.v1": {
"important": {
"total": 2,
"clientIds": ["client1", "client3"],
"clipped": false
},
"urgent": {
"total": 3,
"clientIds": ["client1", "client2", "client3"],
"clipped": false
}
},
"status:unique.v1": {
"important": {
"total": 2,
"clientIds": ["client1", "client3"],
"clipped": false
},
"urgent": {
"total": 1,
"clientIds": ["client2"],
"clipped": false
}
},
"voting:multiple.v1": {
"option-a": {
"total": 7,
"clientCounts": {
"client1": 3,
"client2": 2
},
"totalUnidentified": 2,
"clipped": false,
"totalClientIds": 2
},
"option-b": {
"total": 4,
"clientCounts": {
"client1": 2,
"client3": 1
},
"totalUnidentified": 1,
"clipped": false,
"totalClientIds": 2
}
}
}Large summaries
If many clients publish the same annotation to the same message, the list of client IDs in that annotation summary will get clipped in order to keep the event size within the maximum message size.
When a summary is clipped:
- The
totalproperty shows the total number of annotations as expected, but theclientIdsproperty will contain only a partial list of client IDs. - The
clippedproperty is set totrue. - For the
multipleannotation type, use thetotalClientIdsproperty to determine the total number of clients that have published the annotation. For the other annotation types this is equal tototal.
Subscribe to individual annotation events
It is also possible to subscribe to individual annotation events, as distinct from annotation summaries. These are the emitted when publishing or deleting an annotation.
Summaries are designed to address the most common usecases for annotations, so there is usually no need to subscribe to individual annotation events. However, subscribing to individual annotations is appropriate for usecases where the payload of individual annotation events is relevant to the application.
If you need to, you can subscribe to individual annotation events using the annotations.subscribe() method on a channel. To subscribe to individual annotations, you must request the ANNOTATION_SUBSCRIBE mode.
Annotations delivered to the annotations.subscribe() listener will have an action of annotation.create or annotation.delete.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Create an Ably Realtime client specifying the clientId that will
// be associated with annotations published by this client
const realtime = new Ably.Realtime({ key: 'demokey:*****', clientId: 'my-client-id' });
// Create a channel in a namespace called `annotations`
// which has message annotations enabled.
// Specify the ANNOTATION_SUBSCRIBE mode to enable annotation subscriptions.
const channel = realtime.channels.get('annotations:example', { modes: ['ANNOTATION_SUBSCRIBE', ... /* all other modes you need, such as MESSAGE_SUBSCRIBE */] });
await channel.annotations.subscribe((annotation) => {
if (annotation.action === 'annotation.create') {
console.log(`New ${annotation.type} annotation with name ${annotation.name} from ${annotation.clientId}`);
} else if (annotation.action === 'annotation.delete') {
console.log(`${annotation.clientId} deleted a ${annotation.type} annotation with name ${annotation.name}`);
}
});Annotation message properties
Annotations are a special type of message with the following properties:
| Property | Description |
|---|---|
| id | An Ably-generated ID used to uniquely identify the annotation. |
| action | The action specifies whether this is an annotation being added (annotation.create) or removed (annotation.delete). |
| serial | This annotation's unique serial (lexicographically totally ordered). |
| messageSerial | The serial of the message that this annotation is annotating. |
| type | The annotation type. |
| name | The name of the annotation, used by some annotation types for aggregation. |
| clientId | The client identifier of the user that published this annotation. |
| count | An optional count, only relevant to certain annotation types. |
| data | An optional payload for the annotation. Available on an individual annotation but not aggregated or included in annotation summaries. |
| encoding | This is typically empty, as all annotations received from Ably are automatically decoded client-side using this value. However, if the annotation encoding cannot be processed, this attribute contains the remaining transformations not applied to the data payload. |
| timestamp | The timestamp of when the annotation was received by Ably, as milliseconds since the Unix epoch. |