sendEvent method

Future<String?> sendEvent(
  1. Map<String, dynamic> content, {
  2. String type = EventTypes.Message,
  3. String? txid,
  4. Event? inReplyTo,
  5. String? editEventId,
  6. String? threadRootEventId,
  7. String? threadLastEventId,
})

Sends an event to this room with this json as a content. Returns the event ID generated from the server. It uses list of completer to make sure events are sending in a row.

Implementation

Future<String?> sendEvent(
  Map<String, dynamic> content, {
  String type = EventTypes.Message,
  String? txid,
  Event? inReplyTo,
  String? editEventId,
  String? threadRootEventId,
  String? threadLastEventId,
}) async {
  // Create new transaction id
  final String messageID;
  if (txid == null) {
    messageID = client.generateUniqueTransactionId();
  } else {
    messageID = txid;
  }

  if (inReplyTo != null) {
    var replyText =
        '<${inReplyTo.senderId}> ${_stripBodyFallback(inReplyTo.body)}';
    replyText = replyText.split('\n').map((line) => '> $line').join('\n');
    content['format'] = 'org.matrix.custom.html';
    // be sure that we strip any previous reply fallbacks
    final replyHtml = (inReplyTo.formattedText.isNotEmpty
            ? inReplyTo.formattedText
            : htmlEscape.convert(inReplyTo.body).replaceAll('\n', '<br>'))
        .replaceAll(
      RegExp(
        r'<mx-reply>.*</mx-reply>',
        caseSensitive: false,
        multiLine: false,
        dotAll: true,
      ),
      '',
    );
    final repliedHtml = content.tryGet<String>('formatted_body') ??
        htmlEscape
            .convert(content.tryGet<String>('body') ?? '')
            .replaceAll('\n', '<br>');
    content['formatted_body'] =
        '<mx-reply><blockquote><a href="https://matrix.to/#/${inReplyTo.roomId!}/${inReplyTo.eventId}">In reply to</a> <a href="https://matrix.to/#/${inReplyTo.senderId}">${inReplyTo.senderId}</a><br>$replyHtml</blockquote></mx-reply>$repliedHtml';
    // We escape all @room-mentions here to prevent accidental room pings when an admin
    // replies to a message containing that!
    content['body'] =
        '${replyText.replaceAll('@room', '@\u200broom')}\n\n${content.tryGet<String>('body') ?? ''}';
    content['m.relates_to'] = {
      'm.in_reply_to': {
        'event_id': inReplyTo.eventId,
      },
    };
  }

  if (threadRootEventId != null) {
    content['m.relates_to'] = {
      'event_id': threadRootEventId,
      'rel_type': RelationshipTypes.thread,
      'is_falling_back': inReplyTo == null,
      if (inReplyTo != null) ...{
        'm.in_reply_to': {
          'event_id': inReplyTo.eventId,
        },
      } else ...{
        if (threadLastEventId != null)
          'm.in_reply_to': {
            'event_id': threadLastEventId,
          },
      },
    };
  }

  if (editEventId != null) {
    final newContent = content.copy();
    content['m.new_content'] = newContent;
    content['m.relates_to'] = {
      'event_id': editEventId,
      'rel_type': RelationshipTypes.edit,
    };
    if (content['body'] is String) {
      content['body'] = '* ${content['body']}';
    }
    if (content['formatted_body'] is String) {
      content['formatted_body'] = '* ${content['formatted_body']}';
    }
  }
  final sentDate = DateTime.now();
  final syncUpdate = SyncUpdate(
    nextBatch: '',
    rooms: RoomsUpdate(
      join: {
        id: JoinedRoomUpdate(
          timeline: TimelineUpdate(
            events: [
              MatrixEvent(
                content: content,
                type: type,
                eventId: messageID,
                senderId: client.userID!,
                originServerTs: sentDate,
                unsigned: {
                  messageSendingStatusKey: EventStatus.sending.intValue,
                  'transaction_id': messageID,
                },
              ),
            ],
          ),
        ),
      },
    ),
  );
  // we need to add the transaction ID to the set of events that are currently queued to be sent
  // even before the fake sync is called, so that the event constructor can check if the event is in the sending state
  sendingQueueEventsByTxId.add(messageID);
  await _handleFakeSync(syncUpdate);
  final completer = Completer();
  sendingQueue.add(completer);
  while (sendingQueue.first != completer) {
    await sendingQueue.first.future;
  }

  final timeoutDate = DateTime.now().add(client.sendTimelineEventTimeout);
  // Send the text and on success, store and display a *sent* event.
  String? res;

  while (res == null) {
    try {
      res = await _sendContent(
        type,
        content,
        txid: messageID,
      );
    } catch (e, s) {
      if (e is MatrixException &&
          e.retryAfterMs != null &&
          !DateTime.now()
              .add(Duration(milliseconds: e.retryAfterMs!))
              .isAfter(timeoutDate)) {
        Logs().w(
          'Ratelimited while sending message, waiting for ${e.retryAfterMs}ms',
        );
        await Future.delayed(Duration(milliseconds: e.retryAfterMs!));
      } else if (e is MatrixException ||
          e is EventTooLarge ||
          DateTime.now().isAfter(timeoutDate)) {
        Logs().w('Problem while sending message', e, s);
        syncUpdate.rooms!.join!.values.first.timeline!.events!.first
            .unsigned![messageSendingStatusKey] = EventStatus.error.intValue;
        await _handleFakeSync(syncUpdate);
        completer.complete();
        sendingQueue.remove(completer);
        sendingQueueEventsByTxId.remove(messageID);
        if (e is EventTooLarge ||
            (e is MatrixException && e.error == MatrixError.M_FORBIDDEN)) {
          rethrow;
        }
        return null;
      } else {
        Logs()
            .w('Problem while sending message: $e Try again in 1 seconds...');
        await Future.delayed(Duration(seconds: 1));
      }
    }
  }
  syncUpdate.rooms!.join!.values.first.timeline!.events!.first
      .unsigned![messageSendingStatusKey] = EventStatus.sent.intValue;
  syncUpdate.rooms!.join!.values.first.timeline!.events!.first.eventId = res;
  await _handleFakeSync(syncUpdate);
  completer.complete();
  sendingQueue.remove(completer);
  sendingQueueEventsByTxId.remove(messageID);
  return res;
}