connect method

void connect(
  1. CallService calls
)

Starts the CallService.heartbeat subscription indicating that this OngoingCall is ready to connect to a media server.

No-op if already connected.

Implementation

void connect(CallService calls) {
  Log.debug('connect($calls)', '$runtimeType');

  _participated = true;

  if (connected || callChatItemId == null || deviceId == null) {
    return;
  }

  CallMemberId id = CallMemberId(_me.userId, deviceId);
  members[_me]?.id = id;
  members.move(_me, id);
  _me = id;

  connected = true;
  _heartbeat?.cancel();
  _heartbeat = calls
      .heartbeat(callChatItemId!, deviceId!)
      .listen(
        (e) async {
          switch (e.kind) {
            case ChatCallEventsKind.initialized:
              Log.debug('heartbeat(): ${e.kind}', '$runtimeType');
              break;

            case ChatCallEventsKind.chatCall:
              Log.debug('heartbeat(): ${e.kind}', '$runtimeType');

              final node = e as ChatCallEventsChatCall;

              _handToggles.clear();

              if (node.call.finishReason != null) {
                // Call is already ended, so remove it.
                calls.remove(chatId.value);
                calls.removeCredentials(node.call.chatId, node.call.id);
              } else {
                call.value = node.call;
                call.refresh();

                if (state.value == OngoingCallState.local) {
                  state.value =
                      node.call.conversationStartedAt == null
                          ? OngoingCallState.pending
                          : OngoingCallState.joining;
                }

                final ChatMembersDialed? dialed = node.call.dialed;
                if (dialed is ChatMembersDialedConcrete) {
                  // Remove the members, who are not connected and still
                  // redialing, that are missing from the [dialed].
                  members.removeWhere(
                    (_, v) =>
                        v.isConnected.isFalse &&
                        v.isDialing.isTrue &&
                        dialed.members.none(
                          (e) => e.user.id == v.id.userId,
                        ) &&
                        node.call.members.none(
                          (e) => e.user.id == v.id.userId,
                        ),
                  );

                  for (final ChatMember m in dialed.members) {
                    _addDialing(m.user.id);
                  }
                } else if (dialed == null) {
                  // Remove the members, who are not connected and still
                  // redialing, since no one is [dialed].
                  members.removeWhere(
                    (_, v) =>
                        v.isConnected.isFalse &&
                        v.isDialing.isTrue &&
                        node.call.members.none(
                          (e) => e.user.id == v.id.userId,
                        ),
                  );
                }

                // Subscribes to the [RxChat.members] changes, adding the dialed
                // users.
                //
                // Additionally handles the case, when [dialed] are
                // [ChatMembersDialedAll], since we need to have a [RxChat] to
                // retrieve the whole list of users this way.
                Future<void> redialAndResubscribe(RxChat? v) async {
                  if (!connected || v == null) {
                    // [OngoingCall] might have been disposed or disconnected
                    // while this [Future] was executing.
                    return;
                  }

                  // Add the redialed members of the call to the [members].
                  if (dialed is ChatMembersDialedAll &&
                      v.chat.value.membersCount <= v.members.perPage) {
                    if (v.members.length < v.chat.value.membersCount) {
                      await v.members.around();
                    }

                    // Check if [ChatCall.dialed] is still [ChatMembersDialedAll].
                    if (call.value?.dialed is ChatMembersDialedAll) {
                      final Iterable<RxUser> dialings = v.members.values
                          .map((e) => e.user)
                          .where(
                            (e) =>
                                e.id != me.id.userId &&
                                dialed.answeredMembers.none(
                                  (a) => a.user.id == e.id,
                                ),
                          );

                      // Remove the members, who are not connected and still
                      // redialing, that are missing from the [dialings].
                      members.removeWhere(
                        (_, v) =>
                            v.isConnected.isFalse &&
                            v.isDialing.isTrue &&
                            dialings.none((e) => e.id == v.id.userId) &&
                            node.call.members.none(
                              (e) => e.user.id == v.id.userId,
                            ),
                      );

                      for (final RxUser e in dialings) {
                        _addDialing(e.id);
                      }
                    }
                  }
                }

                // Retrieve the [RxChat] to subscribe to its [RxChat.members]
                // changes, so that added users are displayed as dialed right
                // away.
                final FutureOr<RxChat?> chatOrFuture = calls.getChat(
                  chatId.value,
                );
                if (chatOrFuture is RxChat?) {
                  redialAndResubscribe(chatOrFuture);
                } else {
                  chatOrFuture.then(redialAndResubscribe);
                }

                members[_me]?.isHandRaised.value =
                    node.call.members
                        .firstWhereOrNull((e) => e.user.id == _me.userId)
                        ?.handRaised ??
                    false;
              }
              break;

            case ChatCallEventsKind.event:
              final versioned = (e as ChatCallEventsEvent).event;
              Log.debug(
                'heartbeat(ChatCallEventsEvent): ${versioned.events.map((e) => e.kind)}',
                '$runtimeType($id)',
              );

              for (final ChatCallEvent event in versioned.events) {
                switch (event.kind) {
                  case ChatCallEventKind.roomReady:
                    final node = event as EventChatCallRoomReady;

                    if (!_background) {
                      await _joinRoom(node.joinLink);
                    }

                    state.value = OngoingCallState.active;
                    break;

                  case ChatCallEventKind.finished:
                    final node = event as EventChatCallFinished;
                    if (node.chatId == chatId.value) {
                      calls.removeCredentials(node.call.chatId, node.call.id);
                      calls.remove(chatId.value);
                    }
                    break;

                  case ChatCallEventKind.memberLeft:
                    final node = event as EventChatCallMemberLeft;
                    if (me.id.userId == node.user.id &&
                        me.id.deviceId == node.deviceId) {
                      calls.remove(chatId.value);
                    }

                    final CallMemberId id = CallMemberId(
                      node.user.id,
                      node.deviceId,
                    );

                    if (members[id]?.isConnected.value == false) {
                      members.remove(id)?.dispose();
                    }

                    if (members.keys.none((e) => e.userId == node.user.id)) {
                      call.value?.members.removeWhere(
                        (e) => e.user.id == node.user.id,
                      );
                    }
                    break;

                  case ChatCallEventKind.memberJoined:
                    final node = event as EventChatCallMemberJoined;

                    final CallMemberId redialedId = CallMemberId(
                      node.user.id,
                      null,
                    );
                    final CallMemberId id = CallMemberId(
                      node.user.id,
                      node.deviceId,
                    );

                    final CallMember? redialed = members[redialedId];
                    if (redialed != null) {
                      redialed.id = id;
                      redialed.isDialing.value = false;
                      redialed.joinedAt.value = node.at;
                      members.move(redialedId, id);
                    }

                    final CallMember? member = members[id];

                    if (member == null) {
                      members[id] = CallMember(
                        id,
                        null,
                        joinedAt: node.at,
                        isHandRaised:
                            call.value?.members
                                .firstWhereOrNull(
                                  (e) => e.user.id == id.userId,
                                )
                                ?.handRaised ??
                            false,
                        isConnected: false,
                      );
                    } else {
                      member.joinedAt.value = node.at;
                    }
                    break;

                  case ChatCallEventKind.handLowered:
                    final node = event as EventChatCallHandLowered;

                    // Ignore the event, if it's our hand and is already lowered.
                    if (node.user.id == _me.userId &&
                        _handToggles.firstOrNull == false) {
                      _handToggles.removeAt(0);
                    } else {
                      for (MapEntry<CallMemberId, CallMember> m in members
                          .entries
                          .where((e) => e.key.userId == node.user.id)) {
                        m.value.isHandRaised.value = false;
                      }
                    }

                    for (ChatCallMember m in (call.value?.members ?? [])
                        .where((e) => e.user.id == node.user.id)) {
                      m.handRaised = false;
                    }
                    break;

                  case ChatCallEventKind.handRaised:
                    final node = event as EventChatCallHandRaised;

                    // Ignore the event, if it's our hand and is already raised.
                    if (node.user.id == _me.userId &&
                        _handToggles.firstOrNull == true) {
                      _handToggles.removeAt(0);
                    } else {
                      for (MapEntry<CallMemberId, CallMember> m in members
                          .entries
                          .where((e) => e.key.userId == node.user.id)) {
                        m.value.isHandRaised.value = true;
                      }
                    }

                    for (ChatCallMember m in (call.value?.members ?? [])
                        .where((e) => e.user.id == node.user.id)) {
                      m.handRaised = true;
                    }
                    break;

                  case ChatCallEventKind.declined:
                    final node = event as EventChatCallDeclined;
                    final CallMemberId id = CallMemberId(node.user.id, null);
                    if (members[id]?.isConnected.value == false) {
                      members.remove(id)?.dispose();
                    }
                    break;

                  case ChatCallEventKind.callMoved:
                    final node = event as EventChatCallMoved;
                    chatId.value = node.newChatId;
                    call.value = node.newCall;

                    connected = false;
                    connect(calls);

                    calls.moveCall(
                      chatId: node.chatId,
                      newChatId: node.newChatId,
                      callId: node.callId,
                      newCallId: node.newCallId,
                    );
                    break;

                  case ChatCallEventKind.redialed:
                    final node = event as EventChatCallMemberRedialed;
                    _addDialing(node.user.id);
                    break;

                  case ChatCallEventKind.answerTimeoutPassed:
                    final node = event as EventChatCallAnswerTimeoutPassed;

                    if (node.user?.id != null) {
                      final CallMemberId id = CallMemberId(
                        node.user!.id,
                        null,
                      );
                      if (members[id]?.isConnected.value == false) {
                        members.remove(id)?.dispose();
                      }
                    } else {
                      call.value?.dialed = null;

                      members.removeWhere((k, v) {
                        if (k.deviceId == null && v.isConnected.isFalse) {
                          v.dispose();
                          return true;
                        }

                        return false;
                      });
                    }
                    break;

                  case ChatCallEventKind.conversationStarted:
                    // TODO: Implement [EventChatCallConversationStarted].
                    break;

                  case ChatCallEventKind.undialed:
                    final node = event as EventChatCallMemberUndialed;

                    final CallMemberId id = CallMemberId(node.user.id, null);
                    if (members[id]?.isConnected.value == false) {
                      members.remove(id)?.dispose();
                    }
                    break;
                }
              }
              break;
          }
        },
        onError: (e) {
          if (e is! ResubscriptionRequiredException) {
            throw e;
          }
        },
      );
}