| 1 | import '../data/models/message.dart'; |
| 2 | |
| 3 | class MessageVariants { |
| 4 | static String groupId(Message message) => |
| 5 | message.variantGroupId?.isNotEmpty == true |
| 6 | ? message.variantGroupId! |
| 7 | : message.id; |
| 8 | |
| 9 | static List<Message> resolveActiveTimeline(List<Message> allMessages) { |
| 10 | if (allMessages.isEmpty) return []; |
| 11 | |
| 12 | final normalized = allMessages.map(_normalizeLegacy).toList(); |
| 13 | final withParents = _ensureParentLinks(normalized); |
| 14 | |
| 15 | final childrenOf = <String, List<Message>>{}; |
| 16 | final roots = <Message>[]; |
| 17 | for (final message in withParents) { |
| 18 | final parentId = message.parentMessageId; |
| 19 | if (parentId == null || parentId.isEmpty) { |
| 20 | roots.add(message); |
| 21 | } else { |
| 22 | childrenOf.putIfAbsent(parentId, () => []).add(message); |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | if (roots.isEmpty) { |
| 27 | return _legacyFlatResolve(normalized); |
| 28 | } |
| 29 | |
| 30 | roots.sort((a, b) { |
| 31 | final order = a.threadOrder.compareTo(b.threadOrder); |
| 32 | if (order != 0) return order; |
| 33 | return a.createdAt.compareTo(b.createdAt); |
| 34 | }); |
| 35 | |
| 36 | final timeline = <Message>[]; |
| 37 | var current = _pickActiveSibling(roots); |
| 38 | while (true) { |
| 39 | timeline.add(current); |
| 40 | final children = childrenOf[current.id]; |
| 41 | if (children == null || children.isEmpty) break; |
| 42 | current = _pickActiveSibling(children); |
| 43 | } |
| 44 | return timeline; |
| 45 | } |
| 46 | |
| 47 | static List<Message> _legacyFlatResolve(List<Message> normalized) { |
| 48 | final byGroup = <String, List<Message>>{}; |
| 49 | for (final message in normalized) { |
| 50 | byGroup.putIfAbsent(groupId(message), () => []).add(message); |
| 51 | } |
| 52 | |
| 53 | final active = <Message>[]; |
| 54 | for (final group in byGroup.values) { |
| 55 | group.sort((a, b) => a.variantIndex.compareTo(b.variantIndex)); |
| 56 | final selected = group.firstWhere( |
| 57 | (m) => m.isActiveVariant, |
| 58 | orElse: () => group.last, |
| 59 | ); |
| 60 | active.add(selected); |
| 61 | } |
| 62 | |
| 63 | active.sort((a, b) { |
| 64 | final orderCompare = a.threadOrder.compareTo(b.threadOrder); |
| 65 | if (orderCompare != 0) return orderCompare; |
| 66 | return a.createdAt.compareTo(b.createdAt); |
| 67 | }); |
| 68 | return active; |
| 69 | } |
| 70 | |
| 71 | static List<Message> _ensureParentLinks(List<Message> messages) { |
| 72 | if (messages.any((m) => m.parentMessageId?.isNotEmpty == true)) { |
| 73 | return messages; |
| 74 | } |
| 75 | |
| 76 | final byThreadOrder = <int, List<Message>>{}; |
| 77 | for (final message in messages) { |
| 78 | byThreadOrder.putIfAbsent(message.threadOrder, () => []).add(message); |
| 79 | } |
| 80 | final orders = byThreadOrder.keys.toList()..sort(); |
| 81 | |
| 82 | String? previousActiveId; |
| 83 | final updated = <Message>[]; |
| 84 | for (final order in orders) { |
| 85 | final group = byThreadOrder[order]!; |
| 86 | for (final message in group) { |
| 87 | updated.add(message.copyWith(parentMessageId: previousActiveId)); |
| 88 | } |
| 89 | final active = group.firstWhere( |
| 90 | (m) => m.isActiveVariant, |
| 91 | orElse: () => group.last, |
| 92 | ); |
| 93 | previousActiveId = active.id; |
| 94 | } |
| 95 | return updated; |
| 96 | } |
| 97 | |
| 98 | static Message _pickActiveSibling(List<Message> siblings) { |
| 99 | final byGroup = <String, List<Message>>{}; |
| 100 | for (final message in siblings) { |
| 101 | byGroup.putIfAbsent(groupId(message), () => []).add(message); |
| 102 | } |
| 103 | |
| 104 | if (byGroup.length == 1) { |
| 105 | final group = byGroup.values.first; |
| 106 | group.sort((a, b) => a.variantIndex.compareTo(b.variantIndex)); |
| 107 | return group.firstWhere( |
| 108 | (m) => m.isActiveVariant, |
| 109 | orElse: () => group.last, |
| 110 | ); |
| 111 | } |
| 112 | |
| 113 | siblings.sort((a, b) { |
| 114 | final order = a.threadOrder.compareTo(b.threadOrder); |
| 115 | if (order != 0) return order; |
| 116 | return a.createdAt.compareTo(b.createdAt); |
| 117 | }); |
| 118 | return siblings.firstWhere( |
| 119 | (m) => m.isActiveVariant, |
| 120 | orElse: () => siblings.first, |
| 121 | ); |
| 122 | } |
| 123 | |
| 124 | static List<Message> variantsForMessage( |
| 125 | List<Message> allMessages, |
| 126 | Message message, |
| 127 | ) { |
| 128 | final gid = groupId(message); |
| 129 | return allMessages.where((m) => groupId(m) == gid).toList() |
| 130 | ..sort((a, b) => a.variantIndex.compareTo(b.variantIndex)); |
| 131 | } |
| 132 | |
| 133 | static int activeVariantIndex(List<Message> variants) { |
| 134 | final index = variants.indexWhere((m) => m.isActiveVariant); |
| 135 | return index >= 0 ? index : variants.length - 1; |
| 136 | } |
| 137 | |
| 138 | static Message _normalizeLegacy(Message message) { |
| 139 | if (message.variantGroupId?.isNotEmpty == true) return message; |
| 140 | return message.copyWith( |
| 141 | variantGroupId: message.id, |
| 142 | variantIndex: 0, |
| 143 | isActiveVariant: true, |
| 144 | ); |
| 145 | } |
| 146 | |
| 147 | static int nextThreadOrder(List<Message> activeTimeline) { |
| 148 | if (activeTimeline.isEmpty) return 0; |
| 149 | return activeTimeline |
| 150 | .map((m) => m.threadOrder) |
| 151 | .reduce((a, b) => a > b ? a : b) + |
| 152 | 1; |
| 153 | } |
| 154 | } |