feat/fix: Все кнопки в экране профиля(кроме жалоб). Фикс ожирения бабла. Новые сообщения теперь не тянут вниз если ты отлистал

This commit is contained in:
Jganenokk
2026-08-04 22:31:00 +07:00
parent 6791a30150
commit e93b392f9a
20 changed files with 2006 additions and 161 deletions
+141
View File
@@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:komet/frontend/screens/chats/chat/retain_offset_physics.dart';
const double _itemHeight = 60;
const double _newestHeight = 84;
const double _viewportHeight = 300;
double? _offsetInList(GlobalKey listKey, GlobalKey itemKey) {
final listBox = listKey.currentContext?.findRenderObject();
final box = itemKey.currentContext?.findRenderObject();
if (listBox is! RenderBox || box is! RenderBox || !box.attached) return null;
return box.localToGlobal(Offset.zero, ancestor: listBox).dy;
}
class _Harness {
_Harness(this.tester, {required this.physics});
final WidgetTester tester;
final ScrollPhysics? physics;
final GlobalKey listKey = GlobalKey();
final ScrollController controller = ScrollController();
final List<String> items = [for (var i = 0; i < 40; i++) 'm$i'];
late final Map<String, GlobalKey> keys = {
for (final id in items) id: GlobalKey(),
'newest': GlobalKey(),
};
Future<void> pump() async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
key: listKey,
height: _viewportHeight,
child: CustomScrollView(
controller: controller,
reverse: true,
physics: physics,
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
if (index == 0) return const SizedBox(height: 0);
final id = items[items.length - index];
return SizedBox(
key: keys[id],
height: id == 'newest' ? _newestHeight : _itemHeight,
child: Text(id),
);
}, childCount: items.length + 1),
),
],
),
),
),
),
);
await tester.pump();
}
String anchorId() => items.firstWhere((id) {
final dy = _offsetInList(listKey, keys[id]!);
return dy != null && dy >= 0 && dy <= _viewportHeight;
});
double dyOf(String id) => _offsetInList(listKey, keys[id]!)!;
}
void main() {
testWidgets('appending to a reversed list drags the view toward the newest '
'message', (tester) async {
final h = _Harness(tester, physics: null);
addTearDown(h.controller.dispose);
await h.pump();
h.controller.jumpTo(600);
await tester.pump();
final anchor = h.anchorId();
final beforeDy = h.dyOf(anchor);
h.items.add('newest');
await h.pump();
expect(h.dyOf(anchor), lessThan(beforeDy - 1));
expect(h.controller.position.pixels, 600);
});
testWidgets('RetainOffsetScrollPhysics holds the view in place when a '
'message is appended', (tester) async {
var retainOnce = false;
final h = _Harness(
tester,
physics: RetainOffsetScrollPhysics(
retain: () {
if (!retainOnce) return false;
retainOnce = false;
return true;
},
),
);
addTearDown(h.controller.dispose);
await h.pump();
h.controller.jumpTo(600);
await tester.pump();
final anchor = h.anchorId();
final beforeDy = h.dyOf(anchor);
retainOnce = true;
h.items.add('newest');
await h.pump();
expect(h.dyOf(anchor), closeTo(beforeDy, 0.5));
expect(h.controller.position.pixels, greaterThan(600));
});
testWidgets('RetainOffsetScrollPhysics stays inert while the flag is unset', (
tester,
) async {
final h = _Harness(
tester,
physics: RetainOffsetScrollPhysics(retain: () => false),
);
addTearDown(h.controller.dispose);
await h.pump();
h.controller.jumpTo(600);
await tester.pump();
final anchor = h.anchorId();
final beforeDy = h.dyOf(anchor);
h.items.add('newest');
await h.pump();
expect(h.dyOf(anchor), lessThan(beforeDy - 1));
expect(h.controller.position.pixels, 600);
});
}
+50
View File
@@ -3,9 +3,11 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:komet/backend/modules/messages.dart';
import 'package:komet/frontend/widgets/message_bubble.dart';
import 'package:komet/l10n/app_localizations.dart';
import 'package:komet/models/attachment.dart';
const int _me = 1;
const int _peer = 7;
const double _photoWidth = 180;
CachedMessage _message({
required String text,
@@ -35,6 +37,36 @@ CachedMessage _message({
: null,
);
CachedMessage _photoReply() => CachedMessage(
id: '1',
accountId: _me,
chatId: 2,
senderId: _peer,
text: 'Вот те раз, не может быть',
time: DateTime(2026, 1, 1, 5, 46).millisecondsSinceEpoch,
status: 'sent',
attachments: [
PhotoAttachment(
baseUrl: 'https://example.com/synthetic.jpg',
width: _photoWidth.toInt(),
height: 240,
),
],
payload: {
'link': {
'type': 'REPLY',
'message': {
'id': '9',
'sender': _me,
'text':
'Эта функция, она для «спамеров - скамеров» и «мутных - анонимов»',
'time': 0,
'attaches': [],
},
},
},
);
Future<void> _pumpColumn(
WidgetTester tester,
List<CachedMessage> messages, {
@@ -183,6 +215,24 @@ void main() {
expect(groupGaps, dialogGaps);
});
testWidgets('a reply above a photo stays inside the photo width', (
tester,
) async {
await _pumpBubble(tester, _photoReply());
final quote = _rectOf(
tester,
find
.ancestor(of: find.text('Вы'), matching: find.byType(Container))
.first,
);
final caption = _rectOf(tester, find.text('Вот те раз, не может быть'));
expect(quote.width, closeTo(_photoWidth - 16, 1));
expect(quote.left, greaterThan(0));
expect(quote.right, lessThanOrEqualTo(caption.left + _photoWidth));
});
testWidgets('a bubble without a header or reply still hugs its text', (
tester,
) async {