handleDiff function

dynamic handleDiff(
  1. dynamic state,
  2. dynamic animatedList,
  3. dynamic diff
)

Implementation

@visibleForTesting
ChatRoomState handleDiff(
  ChatRoomState state, // the current state
  AnimatedListState? animatedList, // the animated list connected to this state
  TimelineItemDiff diff, // the diff to apply
) {
  final action = diff.action();
  // the diff is applied in reverse order as the animated list is provided to UI in reverse .i.e. for improved scrolling
  switch (action) {
    case 'Append':
      List<TimelineItem> incoming =
          diff.values().expect('append diff must contain values').toList();

      final messageList = state.messageList.toList();
      final startLen = messageList.length;
      final messages = Map.fromEntries(state.messages.entries);

      for (final m in incoming) {
        final uniqueId = m.uniqueId();
        messages[uniqueId] = m;
        messageList.insert(0, uniqueId);
      }
      final endLen = messageList.length;

      animatedList?.insertAllItems(0, endLen - startLen);

      return state.copyWith(messageList: messageList, messages: messages);

    case 'Set': // used to update UnableToDecrypt message
      TimelineItem m = diff.value().expect('set diff must contain value');
      final index = diff.index().expect('set diff must contain index');

      final reversedIndex =
          state.messageList.isEmpty ? 0 : state.messageList.length - 1 - index;

      final uniqueId = m.uniqueId();
      if (state.messageList.isEmpty) {
        animatedList?.insertItem(0);
        return state.copyWith(messageList: [uniqueId], messages: {uniqueId: m});
      }
      final messageList = state.messageList.toList();
      final removedItem = messageList.removeAt(reversedIndex);
      messageList.insert(reversedIndex, uniqueId);

      final messages = Map.fromEntries(
        state.messages.entries.where((entry) => entry.key != removedItem),
      );
      messages[uniqueId] = m;

      return state.copyWith(messageList: messageList, messages: messages);
    case 'Insert':
      TimelineItem m = diff.value().expect('insert diff must contain value');
      final index = diff.index().expect('insert diff must contain index');

      final reversedIndex =
          state.messageList.isEmpty ? 0 : state.messageList.length - index;

      return state.copyWithNewMessageAt(reversedIndex, m, animatedList);
    case 'Remove':
      int index = diff.index().expect('remove diff must contain index');

      final reversedIndex =
          state.messageList.isEmpty ? 0 : state.messageList.length - 1 - index;
      return state.copyWithRemovedMessageAt(reversedIndex, animatedList);
    case 'PushBack':
      TimelineItem m = diff.value().expect('push back diff must contain value');

      if (state.messageList.isEmpty) {
        final uniqueId = m.uniqueId();
        animatedList?.insertItem(0);
        return state.copyWith(messageList: [uniqueId], messages: {uniqueId: m});
      }

      return state.copyWithNewMessageAt(0, m, animatedList);
    case 'PushFront':
      TimelineItem m = diff.value().expect(
        'push front diff must contain value',
      );

      return state.copyWithNewMessageAt(
        state.messageList.length,
        m,
        animatedList,
      );
    case 'PopBack':
      if (state.messageList.isEmpty) {
        return state;
      }

      return state.copyWithRemovedMessageAt(0, animatedList);
    case 'PopFront':
      return state.copyWithRemovedMessageAt(
        state.messageList.length - 1,
        animatedList,
      );
    case 'Clear':
      if (state.messageList.isNotEmpty && animatedList != null) {
        animatedList.removeAllItems((b, a) => const SizedBox.shrink());
      }
      return state.copyWith(messageList: [], messages: {});
    case 'Reset':
      List<TimelineItem> incoming =
          diff.values().expect('reset diff must contain values').toList();

      final (messageList, messages) = incoming.fold(
        (List<String>.empty(growable: true), <String, TimelineItem>{}),
        (val, m) {
          final (list, map) = val;
          final uniqueId = m.uniqueId();
          list.insert(0, uniqueId);
          map[uniqueId] = m;
          return (list, map);
        },
      );

      if (animatedList != null) {
        animatedList.removeAllItems((b, a) => const SizedBox.shrink());
        animatedList.insertAllItems(0, messageList.length);
      }
      return state.copyWith(messageList: messageList, messages: messages);
    case 'Truncate':
      if (state.messageList.isEmpty) {
        return state;
      }
      final index = diff.index().expect('truncate diff must contain index');

      final reversedIndex = state.messageList.length - index;

      final messageList = state.messageList.toList();

      final keptMessages = messageList.take(reversedIndex).toList();
      final removedMessages = messageList.skip(reversedIndex).toList();

      if (removedMessages.isEmpty) {
        return state;
      } else {
        if (animatedList != null) {
          for (var i = 0; i < removedMessages.length; i++) {
            animatedList.removeItem(
              reversedIndex + i,
              (a, b) => const SizedBox.shrink(),
            );
          }
        }

        final messages = Map.fromEntries(
          state.messages.entries.where(
            (entry) => !removedMessages.contains(entry.key),
          ),
        );

        return state.copyWith(messageList: keptMessages, messages: messages);
      }
    default:
      _log.severe('Unsupported action $action when diffing room messages');
      break;
  }
  return state;
}