feat: добавил отображение + отправку animoji. Добавил матовое стекло для панели ответа при кастомизационном выборе 'прозрачность'

This commit is contained in:
Jganenokk
2026-07-11 23:46:31 +07:00
parent dfece96a5e
commit 3c6de2c496
15 changed files with 1228 additions and 91 deletions
+49
View File
@@ -0,0 +1,49 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:komet/core/utils/text_format.dart';
FormatRange _animoji(int start, int length, String url) => FormatRange(
format: TextFormat.animoji,
start: start,
length: length,
attributes: {'animojiLottieUrl': url},
);
void main() {
test('single animoji-only message is jumbo', () {
expect(animojiOnlyLottieUrls('❤️', [_animoji(0, 2, 'L1')]), ['L1']);
});
test('several animoji with no other text are jumbo, in order', () {
final urls = animojiOnlyLottieUrls('❤️🔥', [
_animoji(2, 2, 'L2'),
_animoji(0, 2, 'L1'),
]);
expect(urls, ['L1', 'L2']);
});
test('animoji mixed with real text is NOT jumbo', () {
expect(
animojiOnlyLottieUrls('animoji message🤣', [_animoji(15, 2, 'L1')]),
isNull,
);
});
test('plain emoji without an ANIMOJI element is NOT jumbo', () {
expect(animojiOnlyLottieUrls('😀', const []), isNull);
});
test('more than the limit is NOT jumbo', () {
final ranges = [
for (var i = 0; i < 5; i++) _animoji(i * 2, 2, 'L$i'),
];
expect(animojiOnlyLottieUrls('❤️❤️❤️❤️❤️', ranges), isNull);
});
test('whitespace between animoji is allowed', () {
expect(
animojiOnlyLottieUrls('❤️ ❤️', [_animoji(0, 2, 'L1'), _animoji(3, 2, 'L2')]),
['L1', 'L2'],
);
});
}
@@ -0,0 +1,114 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:komet/frontend/widgets/rich_message_controller.dart';
import 'package:komet/models/animoji.dart';
Animoji _a(int id, String emoji, String lottie) =>
Animoji(id: id, emoji: emoji, lottieUrl: lottie);
void main() {
test('standalone animoji builds one ANIMOJI element at offset 0', () {
final c = RichMessageController();
c.insertAnimoji(_a(125, '❤️', 'L1'));
final content = c.buildContent();
expect(content.text, '❤️');
expect(content.elements, [
{
'type': 'ANIMOJI',
'from': 0,
'length': 2,
'entityId': 125,
'attributes': {'animojiLottieUrl': 'L1'},
},
]);
});
test('animoji appended after text gets the correct utf16 offset', () {
final c = RichMessageController();
c.value = const TextEditingValue(
text: 'test',
selection: TextSelection.collapsed(offset: 4),
);
c.insertAnimoji(_a(7, '🤣', 'L2'));
final content = c.buildContent();
expect(content.text, 'test🤣');
expect(content.elements.single['from'], 4);
expect(content.elements.single['length'], 2);
expect(content.elements.single['type'], 'ANIMOJI');
});
test('multiple animoji with surrounding text keep glyph offsets in order', () {
final c = RichMessageController();
c.value = const TextEditingValue(
text: 'a',
selection: TextSelection.collapsed(offset: 1),
);
c.insertAnimoji(_a(1, '❤️', 'L1'));
// caret now after first placeholder; type "b"
final t1 = c.value.text; // "a"
c.value = TextEditingValue(
text: '${t1}b',
selection: TextSelection.collapsed(offset: t1.length + 1),
);
c.insertAnimoji(_a(2, '🔥', 'L3'));
final content = c.buildContent();
expect(content.text, 'a❤️b🔥');
final froms = content.elements
.where((e) => e['type'] == 'ANIMOJI')
.map((e) => e['from'])
.toList();
expect(froms, [1, 4]);
});
testWidgets('built span plain text matches controller text (caret invariant)', (
tester,
) async {
late BuildContext ctx;
await tester.pumpWidget(
WidgetsApp(
color: const Color(0xFF000000),
builder: (context, _) {
ctx = context;
return const SizedBox();
},
),
);
final c = RichMessageController();
c.value = const TextEditingValue(
text: 'hi',
selection: TextSelection.collapsed(offset: 2),
);
c.insertAnimoji(_a(1, '❤️', 'L1'));
c.value = TextEditingValue(
text: '${c.value.text}!',
selection: TextSelection.collapsed(offset: c.value.text.length + 1),
);
final span = c.buildTextSpan(
context: ctx,
style: const TextStyle(fontSize: 16),
withComposing: false,
);
expect(span.toPlainText(), c.text);
});
test('deleting the placeholder char drops the entity', () {
final c = RichMessageController();
c.insertAnimoji(_a(1, '❤️', 'L1'));
expect(c.value.text.length, 1);
// backspace: remove the placeholder
c.value = const TextEditingValue(
text: '',
selection: TextSelection.collapsed(offset: 0),
);
final content = c.buildContent();
expect(content.text, '');
expect(content.elements, isEmpty);
});
}