matrix-dart-sdk/test/voip_reactions_test.dart

1343 lines
45 KiB
Dart

import 'dart:async';
import 'package:test/test.dart';
import 'package:matrix/matrix.dart';
import 'fake_client.dart';
import 'webrtc_stub.dart';
void main() {
late Client matrix;
late Room room;
late VoIP voip;
final testEmojis = [
{'emoji': '🖐️', 'name': 'hand raise'},
{'emoji': '👍', 'name': 'thumbs up'},
{'emoji': '👏', 'name': 'clap'},
{'emoji': '❤️', 'name': 'heart'},
{'emoji': '😂', 'name': 'laugh'},
];
group('VoIP Reaction Events Tests', () {
Logs().level = Level.info;
setUp(() async {
matrix = await getClient();
await matrix.abortSync();
voip = VoIP(matrix, MockWebRTCDelegate());
VoIP.customTxid = '1234';
final id = '!calls:example.com';
room = matrix.getRoomById(id)!;
});
test('Test hand raise reaction receiving and state management', () async {
// Set up a group call membership
final membership = CallMembership(
userId: '@alice:testing.com',
callId: 'test_call_reactions',
backend: MeshBackend(),
deviceId: 'device123',
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'membership_event_reactions',
);
// Set up the room state with the membership
room.setState(
Event(
content: {
'memberships': [membership.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'membership_event_reactions',
senderId: '@alice:testing.com',
originServerTs: DateTime.now(),
room: room,
stateKey: '@alice:testing.com',
),
);
// Manually create the group call session since room.setState doesn't trigger the VoIP listener
await voip.createGroupCallFromRoomStateEvent(membership);
// Get the group call session
final groupCall = voip.getGroupCallById(room.id, 'test_call_reactions');
expect(groupCall, isNotNull);
// Enter the group call so it can process reactions
await groupCall!.enter();
// Listen for reaction events
final reactionEvents = <CallReactionEvent>[];
final subscription =
groupCall.matrixRTCEventStream.stream.listen((event) {
if (event is CallReactionEvent) {
reactionEvents.add(event);
}
});
// Simulate receiving a hand raise reaction event
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': '🖐️',
'name': 'hand raise',
'is_ephemeral': true,
'call_id': 'test_call_reactions',
'device_id': 'device123',
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'membership_event_reactions',
},
},
senderId: '@alice:testing.com',
eventId: 'hand_raise_reaction',
originServerTs: DateTime.now(),
),
],
),
),
},
),
),
);
// Wait for the reaction to be processed
await Future.delayed(Duration(milliseconds: 100));
// Verify the reaction was received
expect(reactionEvents.length, 1);
expect(reactionEvents.first, isA<CallReactionAddedEvent>());
final addedEvent = reactionEvents.first as CallReactionAddedEvent;
expect(addedEvent.reactionKey, '🖐️');
expect(addedEvent.participant.userId, '@alice:testing.com');
expect(addedEvent.membershipEventId, 'membership_event_reactions');
await subscription.cancel();
});
test('Test hand raise reaction removal via redaction', () async {
// Set up a group call membership
final membership = CallMembership(
userId: '@alice:testing.com',
callId: 'test_call_redaction',
backend: MeshBackend(),
deviceId: 'device123',
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'membership_event_redaction',
);
// Set up the room state with the membership
room.setState(
Event(
content: {
'memberships': [membership.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'membership_event_redaction',
senderId: '@alice:testing.com',
originServerTs: DateTime.now(),
room: room,
stateKey: '@alice:testing.com',
),
);
// Manually create the group call session
await voip.createGroupCallFromRoomStateEvent(membership);
// Get the group call session
final groupCall = voip.getGroupCallById(room.id, 'test_call_redaction');
expect(groupCall, isNotNull);
// Enter the group call so it can process reactions
await groupCall!.enter();
// Listen for reaction events
final reactionEvents = <CallReactionEvent>[];
final subscription =
groupCall.matrixRTCEventStream.stream.listen((event) {
if (event is CallReactionEvent) {
reactionEvents.add(event);
}
});
// First, add a hand raise reaction
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something1',
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': '🖐️',
'name': 'hand raise',
'is_ephemeral': true,
'call_id': 'test_call_redaction',
'device_id': 'device123',
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'membership_event_redaction',
},
},
senderId: '@alice:testing.com',
eventId: 'hand_raise_to_redact',
originServerTs: DateTime.now(),
),
],
),
),
},
),
),
);
// Wait for processing
await Future.delayed(Duration(milliseconds: 50));
// Now simulate a redaction event
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something2',
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: EventTypes.Redaction,
content: {
'redacts': 'hand_raise_to_redact',
'device_id': 'device123',
'redacts_type': 'com.famedly.call.member.reaction',
'call_id': 'test_call_redaction',
'reason': 'Hand lowered',
},
senderId: '@alice:testing.com',
eventId: 'redaction_event',
originServerTs: DateTime.now(),
),
],
),
),
},
),
),
);
// Wait for processing
await Future.delayed(Duration(milliseconds: 100));
// Verify both events were received
expect(reactionEvents.length, 2);
expect(reactionEvents[0], isA<CallReactionAddedEvent>());
expect(reactionEvents[1], isA<CallReactionRemovedEvent>());
final addedEvent = reactionEvents[0] as CallReactionAddedEvent;
final removedEvent = reactionEvents[1] as CallReactionRemovedEvent;
expect(addedEvent.reactionKey, '🖐️');
expect(removedEvent.redactedEventId, 'hand_raise_to_redact');
expect(removedEvent.participant.userId, '@alice:testing.com');
await subscription.cancel();
});
test('Test multiple participants hand raise reactions', () async {
// Set up multiple group call memberships
final membership1 = CallMembership(
userId: '@user1:testing.com',
callId: 'test_call_multi_reactions',
backend: MeshBackend(),
deviceId: 'device1',
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'membership_event_1',
);
final membership2 = CallMembership(
userId: '@user2:testing.com',
callId: 'test_call_multi_reactions',
backend: MeshBackend(),
deviceId: 'device2',
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'membership_event_2',
);
// Set up the room state with both memberships
room.setState(
Event(
content: {
'memberships': [membership1.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'membership_event_1',
senderId: '@user1:testing.com',
originServerTs: DateTime.now(),
room: room,
stateKey: '@user1:testing.com',
),
);
room.setState(
Event(
content: {
'memberships': [membership2.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'membership_event_2',
senderId: '@user2:testing.com',
originServerTs: DateTime.now(),
room: room,
stateKey: '@user2:testing.com',
),
);
// Manually create the group call sessions
await voip.createGroupCallFromRoomStateEvent(membership1);
await voip.createGroupCallFromRoomStateEvent(membership2);
// Get the group call session
final groupCall =
voip.getGroupCallById(room.id, 'test_call_multi_reactions');
expect(groupCall, isNotNull);
// Enter the group call so it can process reactions
await groupCall!.enter();
// Listen for reaction events
final reactionEvents = <CallReactionEvent>[];
final subscription =
groupCall.matrixRTCEventStream.stream.listen((event) {
if (event is CallReactionEvent) {
reactionEvents.add(event);
}
});
// Simulate multiple hand raise reactions
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': '🖐️',
'name': 'hand raise',
'is_ephemeral': true,
'call_id': 'test_call_multi_reactions',
'device_id': 'device1',
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'membership_event_1',
},
},
senderId: '@user1:testing.com',
eventId: 'hand_raise_user1',
originServerTs: DateTime.now(),
),
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': '🖐️',
'name': 'hand raise',
'is_ephemeral': true,
'call_id': 'test_call_multi_reactions',
'device_id': 'device2',
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'membership_event_2',
},
},
senderId: '@user2:testing.com',
eventId: 'hand_raise_user2',
originServerTs: DateTime.now(),
),
],
),
),
},
),
),
);
// Wait for processing
await Future.delayed(Duration(milliseconds: 100));
// Verify both reactions were received
expect(reactionEvents.length, 2);
expect(
reactionEvents.every((event) => event is CallReactionAddedEvent),
true,
);
final event1 = reactionEvents[0] as CallReactionAddedEvent;
final event2 = reactionEvents[1] as CallReactionAddedEvent;
expect(event1.reactionKey, '🖐️');
expect(event2.reactionKey, '🖐️');
expect(event1.participant.userId, '@user1:testing.com');
expect(event2.participant.userId, '@user2:testing.com');
await subscription.cancel();
});
test('Test current user own reaction events are processed', () async {
// Set up a group call membership for the current user
final membership = CallMembership(
userId: matrix.userID!,
callId: 'test_call_own_reactions',
backend: MeshBackend(),
deviceId: matrix.deviceID!,
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'my_membership_event',
);
// Set up the room state with the membership
room.setState(
Event(
content: {
'memberships': [membership.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'my_membership_event',
senderId: matrix.userID!,
originServerTs: DateTime.now(),
room: room,
stateKey: matrix.userID!,
),
);
// Manually create the group call session
await voip.createGroupCallFromRoomStateEvent(membership);
// Get the group call session
final groupCall =
voip.getGroupCallById(room.id, 'test_call_own_reactions');
expect(groupCall, isNotNull);
// Enter the group call so it can process reactions
await groupCall!.enter();
// Listen for reaction events
final reactionEvents = <CallReactionEvent>[];
final subscription =
groupCall.matrixRTCEventStream.stream.listen((event) {
if (event is CallReactionEvent) {
reactionEvents.add(event);
}
});
// Simulate the current user raising their hand
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': '🖐️',
'name': 'hand raise',
'is_ephemeral': true,
'call_id': 'test_call_own_reactions',
'device_id': matrix.deviceID!,
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'my_membership_event',
},
},
senderId: matrix.userID!,
eventId: 'my_hand_raise',
originServerTs: DateTime.now(),
),
],
),
),
},
),
),
);
// Wait for processing
await Future.delayed(Duration(milliseconds: 100));
// Verify the user's own reaction was processed
expect(reactionEvents.length, 1);
expect(reactionEvents.first, isA<CallReactionAddedEvent>());
final addedEvent = reactionEvents.first as CallReactionAddedEvent;
expect(addedEvent.reactionKey, '🖐️');
expect(addedEvent.participant.userId, matrix.userID!);
expect(addedEvent.membershipEventId, 'my_membership_event');
await subscription.cancel();
});
test('Test sending hand raise reaction through GroupCallSession', () async {
// Set up a group call membership for the current user
final membership = CallMembership(
userId: matrix.userID!,
callId: 'test_call_send_reaction',
backend: MeshBackend(),
deviceId: matrix.deviceID!,
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'send_membership_event',
);
// Set up the room state with the membership
room.setState(
Event(
content: {
'memberships': [membership.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'send_membership_event',
senderId: matrix.userID!,
originServerTs: DateTime.now(),
room: room,
stateKey: matrix.userID!,
),
);
// Manually create the group call session
await voip.createGroupCallFromRoomStateEvent(membership);
// Get the group call session
final groupCall =
voip.getGroupCallById(room.id, 'test_call_send_reaction');
expect(groupCall, isNotNull);
// Enter the group call so it can process reactions
await groupCall!.enter();
// Test sending a hand raise reaction
// This will fail with the fake client but should not crash
final eventId = await groupCall.sendReactionEvent(emoji: '🖐️');
// With fake client, this will return a valid event ID now that we added the endpoint
expect(eventId, isNotNull); // Expected with updated fake client
// Test removing a reaction (will also fail with fake client)
await groupCall.removeReactionEvent(eventId: 'fake_reaction_id');
// The test passes if we reach here without crashing
expect(true, true);
});
test('Test getAllReactions includes current user reactions', () async {
// Set up group call memberships for multiple users including current user
final currentUserMembership = CallMembership(
userId: matrix.userID!,
callId: 'test_call_get_all',
backend: MeshBackend(),
deviceId: matrix.deviceID!,
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'current_user_membership',
);
final otherUserMembership = CallMembership(
userId: '@other:testing.com',
callId: 'test_call_get_all',
backend: MeshBackend(),
deviceId: 'other_device',
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'other_user_membership',
);
// Set up the room state with both memberships
room.setState(
Event(
content: {
'memberships': [currentUserMembership.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'current_user_membership',
senderId: matrix.userID!,
originServerTs: DateTime.now(),
room: room,
stateKey: matrix.userID!,
),
);
room.setState(
Event(
content: {
'memberships': [otherUserMembership.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'other_user_membership',
senderId: '@other:testing.com',
originServerTs: DateTime.now(),
room: room,
stateKey: '@other:testing.com',
),
);
// Manually create the group call sessions
await voip.createGroupCallFromRoomStateEvent(currentUserMembership);
await voip.createGroupCallFromRoomStateEvent(otherUserMembership);
// Get the group call session
final groupCall = voip.getGroupCallById(room.id, 'test_call_get_all');
expect(groupCall, isNotNull);
// Enter the group call so it can process reactions
await groupCall!.enter();
// Test getAllReactions method (will return empty with fake client but should not crash)
final reactions = await groupCall.getAllReactions(emoji: '🖐️');
// With fake client, this will return empty list, but the method should exist and be callable
expect(reactions, isA<List<MatrixEvent>>());
// The test passes if we reach here without crashing
expect(true, true);
});
test('Test invalid reaction events are ignored', () async {
// Set up a group call membership
final membership = CallMembership(
userId: '@alice:testing.com',
callId: 'test_call_invalid_reactions',
backend: MeshBackend(),
deviceId: 'device123',
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'membership_event_invalid_reactions',
);
// Set up the room state with the membership
room.setState(
Event(
content: {
'memberships': [membership.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'membership_event_invalid_reactions',
senderId: '@alice:testing.com',
originServerTs: DateTime.now(),
room: room,
stateKey: '@alice:testing.com',
),
);
// Manually create the group call session
await voip.createGroupCallFromRoomStateEvent(membership);
// Get the group call session
final groupCall =
voip.getGroupCallById(room.id, 'test_call_invalid_reactions');
expect(groupCall, isNotNull);
// Enter the group call so it can process reactions
await groupCall!.enter();
// Listen for reaction events
final reactionEvents = <CallReactionEvent>[];
final subscription =
groupCall.matrixRTCEventStream.stream.listen((event) {
if (event is CallReactionEvent) {
reactionEvents.add(event);
}
});
// Test invalid reaction events
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(
events: [
// Missing m.relates_to
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': '🖐️',
'name': 'hand raise',
'is_ephemeral': true,
'call_id': 'test_call_invalid_reactions',
'device_id': 'device123',
// Missing m.relates_to
},
senderId: '@alice:testing.com',
eventId: 'invalid_reaction_1',
originServerTs: DateTime.now(),
),
// Missing key in content
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
// Missing key
'name': 'hand raise',
'is_ephemeral': true,
'call_id': 'test_call_invalid_reactions',
'device_id': 'device123',
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'membership_event_invalid_reactions',
},
},
senderId: '@alice:testing.com',
eventId: 'invalid_reaction_2',
originServerTs: DateTime.now(),
),
// Missing device_id
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': '🖐️',
'name': 'hand raise',
'is_ephemeral': true,
'call_id': 'test_call_invalid_reactions',
// Missing device_id
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'membership_event_invalid_reactions',
},
},
senderId: '@alice:testing.com',
eventId: 'invalid_reaction_3',
originServerTs: DateTime.now(),
),
],
),
),
},
),
),
);
// Wait for processing
await Future.delayed(Duration(milliseconds: 100));
// Verify no invalid reactions were processed
expect(reactionEvents.isEmpty, true);
await subscription.cancel();
});
test('Test invalid redaction events are ignored', () async {
// Set up a group call membership
final membership = CallMembership(
userId: '@alice:testing.com',
callId: 'test_call_invalid_redactions',
backend: MeshBackend(),
deviceId: 'device123',
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'membership_event_invalid_redactions',
);
// Set up the room state with the membership
room.setState(
Event(
content: {
'memberships': [membership.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'membership_event_invalid_redactions',
senderId: '@alice:testing.com',
originServerTs: DateTime.now(),
room: room,
stateKey: '@alice:testing.com',
),
);
// Manually create the group call session
await voip.createGroupCallFromRoomStateEvent(membership);
// Get the group call session
final groupCall =
voip.getGroupCallById(room.id, 'test_call_invalid_redactions');
expect(groupCall, isNotNull);
// Enter the group call so it can process reactions
await groupCall!.enter();
// Listen for reaction events
final reactionEvents = <CallReactionEvent>[];
final subscription =
groupCall.matrixRTCEventStream.stream.listen((event) {
if (event is CallReactionEvent) {
reactionEvents.add(event);
}
});
// Test invalid redaction events
await matrix.handleSync(
SyncUpdate(
nextBatch: 'something',
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(
events: [
// Missing redacts field
MatrixEvent(
type: EventTypes.Redaction,
content: {
// Missing redacts
'reason': 'Hand lowered',
},
senderId: '@alice:testing.com',
eventId: 'invalid_redaction_1',
originServerTs: DateTime.now(),
),
// Empty content
MatrixEvent(
type: EventTypes.Redaction,
content: {},
senderId: '@alice:testing.com',
eventId: 'invalid_redaction_2',
originServerTs: DateTime.now(),
),
],
),
),
},
),
),
);
// Wait for processing
await Future.delayed(Duration(milliseconds: 100));
// Verify no invalid redactions were processed
expect(reactionEvents.isEmpty, true);
await subscription.cancel();
});
test('Test different emoji reactions (thumbs up, clap, heart)', () async {
// Set up a group call membership
final membership = CallMembership(
userId: '@alice:testing.com',
callId: 'test_call_emoji_variety',
backend: MeshBackend(),
deviceId: 'device123',
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'membership_event_emoji_variety',
);
// Set up the room state with the membership
room.setState(
Event(
content: {
'memberships': [membership.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'membership_event_emoji_variety',
senderId: '@alice:testing.com',
originServerTs: DateTime.now(),
room: room,
stateKey: '@alice:testing.com',
),
);
// Manually create the group call session
await voip.createGroupCallFromRoomStateEvent(membership);
// Get the group call session
final groupCall =
voip.getGroupCallById(room.id, 'test_call_emoji_variety');
expect(groupCall, isNotNull);
// Enter the group call so it can process reactions
await groupCall!.enter();
// Listen for reaction events
final reactionEvents = <CallReactionEvent>[];
final subscription =
groupCall.matrixRTCEventStream.stream.listen((event) {
if (event is CallReactionEvent) {
reactionEvents.add(event);
}
});
// Test different emoji reactions using first 5 emojis
final emojis = testEmojis.take(5).toList();
for (int i = 0; i < emojis.length; i++) {
await matrix.handleSync(
SyncUpdate(
nextBatch: 'emoji_batch_$i',
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(
events: [
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': emojis[i]['emoji']!,
'name': emojis[i]['name']!,
'is_ephemeral': true,
'call_id': 'test_call_emoji_variety',
'device_id': 'device123',
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'membership_event_emoji_variety',
},
},
senderId: '@alice:testing.com',
eventId: 'emoji_reaction_$i',
originServerTs: DateTime.now(),
),
],
),
),
},
),
),
);
// Small delay between reactions
await Future.delayed(Duration(milliseconds: 10));
}
// Wait for all reactions to be processed
await Future.delayed(Duration(milliseconds: 100));
// Verify all emoji reactions were received
expect(reactionEvents.length, emojis.length);
expect(
reactionEvents.every((event) => event is CallReactionAddedEvent),
true,
);
// Verify each emoji was processed correctly
for (int i = 0; i < emojis.length; i++) {
final event = reactionEvents[i] as CallReactionAddedEvent;
expect(event.reactionKey, emojis[i]['emoji']);
expect(event.participant.userId, '@alice:testing.com');
expect(event.isEphemeral, true);
}
await subscription.cancel();
});
test('Test ephemeral vs permanent reactions', () async {
// Set up a group call membership
final membership = CallMembership(
userId: '@alice:testing.com',
callId: 'test_call_ephemeral_permanent',
backend: MeshBackend(),
deviceId: 'device123',
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'membership_event_ephemeral_permanent',
);
// Set up the room state with the membership
room.setState(
Event(
content: {
'memberships': [membership.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'membership_event_ephemeral_permanent',
senderId: '@alice:testing.com',
originServerTs: DateTime.now(),
room: room,
stateKey: '@alice:testing.com',
),
);
// Manually create the group call session
await voip.createGroupCallFromRoomStateEvent(membership);
// Get the group call session
final groupCall =
voip.getGroupCallById(room.id, 'test_call_ephemeral_permanent');
expect(groupCall, isNotNull);
// Enter the group call so it can process reactions
await groupCall!.enter();
// Listen for reaction events
final reactionEvents = <CallReactionEvent>[];
final subscription =
groupCall.matrixRTCEventStream.stream.listen((event) {
if (event is CallReactionEvent) {
reactionEvents.add(event);
}
});
// Send both ephemeral and permanent reactions
await matrix.handleSync(
SyncUpdate(
nextBatch: 'ephemeral_permanent_batch',
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(
events: [
// Ephemeral reaction
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': testEmojis[0]['emoji']!,
'name': testEmojis[0]['name']!,
'is_ephemeral': true,
'call_id': 'test_call_ephemeral_permanent',
'device_id': 'device123',
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'membership_event_ephemeral_permanent',
},
},
senderId: '@alice:testing.com',
eventId: 'ephemeral_reaction',
originServerTs: DateTime.now(),
),
// Permanent reaction
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': testEmojis[1]['emoji']!,
'name': testEmojis[1]['name']!,
'is_ephemeral': false,
'call_id': 'test_call_ephemeral_permanent',
'device_id': 'device123',
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'membership_event_ephemeral_permanent',
},
},
senderId: '@alice:testing.com',
eventId: 'permanent_reaction',
originServerTs: DateTime.now(),
),
],
),
),
},
),
),
);
// Wait for processing
await Future.delayed(Duration(milliseconds: 100));
// Verify both reactions were received
expect(reactionEvents.length, 2);
expect(
reactionEvents.every((event) => event is CallReactionAddedEvent),
true,
);
final ephemeralEvent = reactionEvents[0] as CallReactionAddedEvent;
final permanentEvent = reactionEvents[1] as CallReactionAddedEvent;
// Verify ephemeral reaction properties
expect(ephemeralEvent.reactionKey, testEmojis[0]['emoji']);
expect(ephemeralEvent.isEphemeral, true);
expect(ephemeralEvent.participant.userId, '@alice:testing.com');
// Verify permanent reaction properties
expect(permanentEvent.reactionKey, testEmojis[1]['emoji']);
expect(permanentEvent.isEphemeral, false);
expect(permanentEvent.participant.userId, '@alice:testing.com');
await subscription.cancel();
});
test('Test ephemeral reaction timeout handling', () async {
// Set up a group call membership
final membership = CallMembership(
userId: '@alice:testing.com',
callId: 'test_call_ephemeral_timeout',
backend: MeshBackend(),
deviceId: 'device123',
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'membership_event_ephemeral_timeout',
);
// Set up the room state with the membership
room.setState(
Event(
content: {
'memberships': [membership.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'membership_event_ephemeral_timeout',
senderId: '@alice:testing.com',
originServerTs: DateTime.now(),
room: room,
stateKey: '@alice:testing.com',
),
);
// Manually create the group call session
await voip.createGroupCallFromRoomStateEvent(membership);
// Get the group call session
final groupCall =
voip.getGroupCallById(room.id, 'test_call_ephemeral_timeout');
expect(groupCall, isNotNull);
// Enter the group call so it can process reactions
await groupCall!.enter();
// Listen for reaction events
final reactionEvents = <CallReactionEvent>[];
final subscription =
groupCall.matrixRTCEventStream.stream.listen((event) {
if (event is CallReactionEvent) {
reactionEvents.add(event);
}
});
// Create an old ephemeral reaction (older than timeout)
final oldTimestamp = DateTime.now().subtract(Duration(minutes: 10));
await matrix.handleSync(
SyncUpdate(
nextBatch: 'timeout_batch',
rooms: RoomsUpdate(
join: {
room.id: JoinedRoomUpdate(
timeline: TimelineUpdate(
events: [
// Old ephemeral reaction (should be ignored)
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': testEmojis[0]['emoji']!,
'name': 'old ${testEmojis[0]['name']}',
'is_ephemeral': true,
'call_id': 'test_call_ephemeral_timeout',
'device_id': 'device123',
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'membership_event_ephemeral_timeout',
},
},
senderId: '@alice:testing.com',
eventId: 'old_ephemeral_reaction',
originServerTs: oldTimestamp,
),
// Recent ephemeral reaction (should be processed)
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': testEmojis[1]['emoji']!,
'name': 'recent ${testEmojis[1]['name']}',
'is_ephemeral': true,
'call_id': 'test_call_ephemeral_timeout',
'device_id': 'device123',
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'membership_event_ephemeral_timeout',
},
},
senderId: '@alice:testing.com',
eventId: 'recent_ephemeral_reaction',
originServerTs: DateTime.now(),
),
// Old permanent reaction (should still be processed)
MatrixEvent(
type: EventTypes.GroupCallMemberReaction,
content: {
'key': testEmojis[3]['emoji']!,
'name': 'old ${testEmojis[3]['name']}',
'is_ephemeral': false,
'call_id': 'test_call_ephemeral_timeout',
'device_id': 'device123',
'm.relates_to': {
'rel_type': RelationshipTypes.reference,
'event_id': 'membership_event_ephemeral_timeout',
},
},
senderId: '@alice:testing.com',
eventId: 'old_permanent_reaction',
originServerTs: oldTimestamp,
),
],
),
),
},
),
),
);
// Wait for processing
await Future.delayed(Duration(milliseconds: 100));
// Verify only recent ephemeral and old permanent reactions were processed
// Old ephemeral should be ignored due to timeout
expect(reactionEvents.length, 2);
final processedReactions = reactionEvents.cast<CallReactionAddedEvent>();
// Should have recent ephemeral thumbs up
expect(
processedReactions.any(
(event) =>
event.reactionKey == testEmojis[1]['emoji'] &&
event.isEphemeral == true,
),
true,
);
// Should have old permanent heart
expect(
processedReactions.any(
(event) =>
event.reactionKey == testEmojis[3]['emoji'] &&
event.isEphemeral == false,
),
true,
);
// Should NOT have old ephemeral hand raise
expect(
processedReactions
.any((event) => event.reactionKey == testEmojis[0]['emoji']),
false,
);
await subscription.cancel();
});
test('Test sending different emoji types through GroupCallSession',
() async {
// Set up a group call membership for the current user
final membership = CallMembership(
userId: matrix.userID!,
callId: 'test_call_send_emojis',
backend: MeshBackend(),
deviceId: matrix.deviceID!,
expiresTs:
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch,
roomId: room.id,
membershipId: voip.currentSessionId,
voip: voip,
eventId: 'send_emojis_membership_event',
);
// Set up the room state with the membership
room.setState(
Event(
content: {
'memberships': [membership.toJson()],
},
type: EventTypes.GroupCallMember,
eventId: 'send_emojis_membership_event',
senderId: matrix.userID!,
originServerTs: DateTime.now(),
room: room,
stateKey: matrix.userID!,
),
);
// Manually create the group call session
await voip.createGroupCallFromRoomStateEvent(membership);
// Get the group call session
final groupCall = voip.getGroupCallById(room.id, 'test_call_send_emojis');
expect(groupCall, isNotNull);
// Enter the group call so it can process reactions
await groupCall!.enter();
// Test sending different emoji reactions using all test emojis
for (final emojiData in testEmojis) {
// Test ephemeral reaction
final ephemeralEventId = await groupCall.sendReactionEvent(
emoji: emojiData['emoji']!,
isEphemeral: true,
);
expect(ephemeralEventId, isNotNull);
// Test permanent reaction
final permanentEventId = await groupCall.sendReactionEvent(
emoji: emojiData['emoji']!,
isEphemeral: false,
);
expect(permanentEventId, isNotNull);
// Small delay between sends
await Future.delayed(Duration(milliseconds: 10));
}
// Test removing reactions (will work with fake client)
await room.redactEvent('fake_reaction_id_1');
await room.redactEvent('fake_reaction_id_2');
// The test passes if we reach here without crashing
expect(true, true);
});
});
}