Files
sepot/app/test/message_receipts_test.dart
T
Julius Joska 5246a97db2 feat(app): redesigned interface, polls, drafts, forwarding and mentions
A new look across the app and a set of everyday messaging features.

Interface
- Home screen with Chats, Contacts and Settings tabs, a large title,
  search across chats and message text (diacritics-insensitive), filters
  and pinned chats.
- Conversation view with a translucent header and composer, grouped
  bubbles, author avatars and colours in groups, swipe to reply, an
  "N new messages" marker, a jump-to-latest button and in-chat search.
- Contact and group profiles: verification, shared photos, groups in
  common, disappearing messages, mute, block; members, admin badges,
  adding people, rename and leave for groups.
- Full-screen photo viewer that pages through the whole conversation.
- A stable avatar colour per person and a clearly visible read state.

Messaging
- Polls in chats and groups, single or multiple choice, with votes that
  can be changed or withdrawn; older clients see them as text.
- Drafts survive leaving a chat and show in the chat list.
- Forwarding of messages, photos, files and voice messages.
- Notes to self: a local-only, encrypted chat that never reaches the
  relay.
- @mentions in groups that notify even when the group is muted.
- Typing indicators sent as ephemeral messages, opt-in and mutual like
  read receipts.
- Delivery and read times per message and per group member.
- Profile photos, cropped, resized and stripped of metadata, delivered
  encrypted to contacts.
- Voice message playback at 1.5x and 2x.
- Call buttons in 1:1 chats and call log entries.

Fixes
- Muted groups no longer notify: muting was checked against the
  author's chat instead of the group.
- A blocked contact could unblock itself by changing its delivery
  address.
- Read receipts no longer regress to delivered when a late delivery
  receipt arrives.
- A message marked as failed corrects itself once it is delivered.
- Double ticks in groups now mean delivered to every member.
2026-10-03 22:44:53 +02:00

95 lines
3.4 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:sepot/core/services/messaging_service.dart';
import 'package:sepot/models/message.dart';
/// Doručenky a potvrzení přečtení nesou čas od příjemce; stav mé zprávy
/// z nich roste, ale nikdy neklesá.
void main() {
final odeslano = DateTime(2026, 9, 28, 14, 0);
Message moje({MessageStatus status = MessageStatus.sent}) => Message(
id: 'm1',
senderId: 'ja',
recipientId: 'bob',
type: MessageType.text,
content: 'ahoj',
timestamp: odeslano,
status: status,
);
Message receipt(Message m, String from, int minut,
{bool read = false, int recipients = 1}) =>
MessagingService.applyReceipt(
m,
from: from,
at: odeslano.add(Duration(minutes: minut)),
read: read,
recipients: recipients,
);
test('doručenka zapíše čas a povýší na doručeno', () {
final m = receipt(moje(), 'bob', 1);
expect(m.status, MessageStatus.delivered);
expect(m.deliveredTo['bob'], odeslano.add(const Duration(minutes: 1)));
expect(m.firstReadAt, isNull);
});
test('přečtení zároveň znamená doručení', () {
final m = receipt(moje(), 'bob', 5, read: true);
expect(m.status, MessageStatus.read);
expect(m.firstDeliveredAt, m.firstReadAt);
});
test('pozdní doručenka po přečtení stav nesníží', () {
// Doručenka poslaná znovu po výpadku spojení dorazí až po „přečteno".
var m = receipt(moje(), 'bob', 5, read: true);
m = receipt(m, 'bob', 9);
expect(m.status, MessageStatus.read);
});
test('opakované potvrzení nepřepíše první čas', () {
var m = receipt(moje(), 'bob', 2);
m = receipt(m, 'bob', 30);
expect(m.deliveredTo['bob'], odeslano.add(const Duration(minutes: 2)));
});
test('doručenka zachrání zprávu označenou jako neodeslaná', () {
// Timeout před ACK relaye → failed, přitom zpráva dorazila.
final m = receipt(moje(status: MessageStatus.failed), 'bob', 1);
expect(m.status, MessageStatus.delivered);
});
test('opožděné hodiny příjemce nepřečtou zprávu před odesláním', () {
final m = receipt(moje(), 'bob', -10, read: true);
expect(m.firstReadAt, odeslano);
});
test('skupina: ✓✓ a přečteno až za všechny ostatní členy', () {
var m = receipt(moje(), 'bob', 1, recipients: 2);
expect(m.status, MessageStatus.sent);
m = receipt(m, 'cyril', 2, recipients: 2);
expect(m.status, MessageStatus.delivered);
m = receipt(m, 'bob', 3, read: true, recipients: 2);
expect(m.status, MessageStatus.delivered);
m = receipt(m, 'cyril', 4, read: true, recipients: 2);
expect(m.status, MessageStatus.read);
expect(m.firstReadAt, odeslano.add(const Duration(minutes: 3)));
});
test('časy přežijí uložení a načtení', () {
final m = receipt(receipt(moje(), 'bob', 1), 'bob', 4, read: true);
final zpet = Message.fromJson(m.toJson());
expect(zpet.deliveredTo, m.deliveredTo);
expect(zpet.readBy, m.readBy);
});
test('starý záznam bez časů i vadné položky se načtou', () {
final json = moje().toJson()
..remove('deliveredTo')
..['readBy'] = {'bob': 'včera', 'cyril': 1790000000000};
final m = Message.fromJson(json);
expect(m.deliveredTo, isEmpty);
expect(m.readBy.keys, ['cyril']);
});
}