Message annotations
Message annotations enable clients to add metadata 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"
When clients publish or delete an annotation, Ably automatically creates a summary that provides an aggregated view of all annotations for that 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:
namespace
is a string (e.g.reactions
) that groups related annotations. Only annotations in the same namespace will be aggregated together to produce summaries.summarization
specifies how annotations are aggregated to produce summaries, such astotal
,flag
,distinct
,unique
, ormultiple
.version
specifies 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 clientId
s. 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 clientId
s 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 name
s.
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 clientId
s 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 clientId
s.
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 name
s.
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 someone subscribing 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, they affect 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 recommended way to receive annotation updates is through annotation summaries. These events provide a summary of the complete, current state of all annotations for a message whenever an annotation is published or deleted.
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
total
property shows the total number of annotations as expected, but theclientIds
property will contain only a partial list of client IDs. - The
clipped
property is set totrue
. - For the
multiple
annotation type, use thetotalClientIds
property 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, rather than annotation summaries. These are the emitted when publishing or deleting an annotation.
Individual events can be useful for activity feeds or detailed logging, but generally, for most usecases, subscribed clients should rely on aggregated summaries. The aggregation of annotations for a message into a summary attached to the message is the primary benefit of using the annotations API; an app design oriented around every client needing to subscribe to raw annotation events may not be taking full advantage of the feature.
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. |