5246a97db2
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.
122 lines
4.5 KiB
Dart
122 lines
4.5 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:sepot/features/profile/group_profile_screen.dart';
|
|
import 'package:sepot/features/profile/profile_widgets.dart';
|
|
import 'package:sepot/l10n/generated/app_localizations.dart';
|
|
import 'package:sepot/models/contact.dart';
|
|
import 'package:sepot/models/group.dart';
|
|
import 'package:sepot/models/identity.dart';
|
|
import 'package:sepot/providers/contacts_provider.dart';
|
|
import 'package:sepot/providers/conversations_provider.dart';
|
|
import 'package:sepot/providers/groups_provider.dart';
|
|
import 'package:sepot/providers/identity_provider.dart';
|
|
|
|
/// Profil skupiny podle role: ne-správce nesmí vidět správcovské akce
|
|
/// (odebrat, udělat správcem), jen vysvětlení; správce je dostane v nabídce
|
|
/// člena.
|
|
class _FakeIdentityNotifier extends IdentityNotifier {
|
|
_FakeIdentityNotifier(this._identity);
|
|
final LocalIdentity _identity;
|
|
|
|
@override
|
|
Future<LocalIdentity?> build() async => _identity;
|
|
}
|
|
|
|
class _NoContacts extends ContactsNotifier {
|
|
@override
|
|
Future<List<Contact>> build() async => const [];
|
|
}
|
|
|
|
final _ja = LocalIdentity(
|
|
displayName: 'Já',
|
|
identityPrivateKey: Uint8List(32),
|
|
identityPublicKey: Uint8List(33),
|
|
createdAt: DateTime.utc(2026, 1, 1),
|
|
);
|
|
|
|
const _spravce = 'SPRAVCE0001';
|
|
const _clen = 'CLENKA0002';
|
|
|
|
Group _group({required bool jaSpravce, required bool vsichniV2}) => Group(
|
|
id: 'g1',
|
|
name: 'Chata v srpnu',
|
|
memberIds: [_ja.id, _spravce, _clen],
|
|
createdBy: _spravce,
|
|
admins: [_spravce, if (jaSpravce) _ja.id],
|
|
v2Members: vsichniV2 ? {_spravce, _clen} : {_spravce},
|
|
);
|
|
|
|
Widget _app(Group group) => ProviderScope(
|
|
overrides: [
|
|
identityProvider.overrideWith(() => _FakeIdentityNotifier(_ja)),
|
|
contactsProvider.overrideWith(_NoContacts.new),
|
|
groupProvider.overrideWith((ref, id) async => group),
|
|
conversationProvider.overrideWith((ref, id) async => null),
|
|
],
|
|
child: MaterialApp(
|
|
locale: const Locale('cs'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: GroupProfileScreen(groupId: group.id),
|
|
),
|
|
);
|
|
|
|
Future<void> _pump(WidgetTester tester, Group group) async {
|
|
tester.view.physicalSize = const Size(1080, 3200);
|
|
tester.view.devicePixelRatio = 2.0;
|
|
addTearDown(tester.view.reset);
|
|
await tester.pumpWidget(_app(group));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
ProfileAction _addAction(WidgetTester tester) => tester.widget<ProfileAction>(
|
|
find.widgetWithText(ProfileAction, 'Přidat'));
|
|
|
|
void main() {
|
|
testWidgets('ne-správce ve vynucené skupině: akce skryté, vysvětlení',
|
|
(tester) async {
|
|
await _pump(tester, _group(jaSpravce: false, vsichniV2: true));
|
|
expect(tester.takeException(), isNull);
|
|
|
|
expect(find.text('Správce'), findsOneWidget,
|
|
reason: 'odznak u skutečného správce');
|
|
expect(find.text('Jméno, členy a správce skupiny mění jen správci.'),
|
|
findsOneWidget);
|
|
expect(_addAction(tester).onTap, isNull,
|
|
reason: 'přidávat lidi smí jen správce');
|
|
expect(find.byIcon(Icons.edit_outlined), findsNothing,
|
|
reason: 'přejmenování jen správcem');
|
|
|
|
await tester.tap(find.textContaining('SEP-CLENKA'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Odebrat ze skupiny'), findsNothing);
|
|
expect(find.text('Udělat správcem'), findsNothing);
|
|
});
|
|
|
|
testWidgets('ne-správce s v1 členem ve skupině smí přidávat, ne spravovat',
|
|
(tester) async {
|
|
await _pump(tester, _group(jaSpravce: false, vsichniV2: false));
|
|
expect(_addAction(tester).onTap, isNotNull);
|
|
expect(find.textContaining('Přidávat lidi zatím může každý'),
|
|
findsOneWidget);
|
|
expect(find.byIcon(Icons.edit_outlined), findsNothing);
|
|
});
|
|
|
|
testWidgets('správce dostane nabídku člena s odebráním a rolí',
|
|
(tester) async {
|
|
await _pump(tester, _group(jaSpravce: true, vsichniV2: true));
|
|
expect(find.text('Správce'), findsNWidgets(2));
|
|
expect(_addAction(tester).onTap, isNotNull);
|
|
expect(find.byIcon(Icons.edit_outlined), findsOneWidget);
|
|
expect(find.textContaining('mění jen správci'), findsNothing);
|
|
|
|
await tester.tap(find.textContaining('SEP-CLENKA'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Odebrat ze skupiny'), findsOneWidget);
|
|
expect(find.text('Udělat správcem'), findsOneWidget);
|
|
});
|
|
}
|