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.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sepot/core/calls/call_controller.dart';
|
||||
import 'package:sepot/core/network/relay_client.dart';
|
||||
import 'package:sepot/core/push/push_notifier.dart';
|
||||
import 'package:sepot/core/services/messaging_service.dart';
|
||||
@@ -16,6 +19,28 @@ final messagingServiceProvider = Provider<MessagingService>((ref) {
|
||||
return service;
|
||||
});
|
||||
|
||||
/// Kdo právě píše: konverzace → otisky pisatelů. Prázdné, když je
|
||||
/// „píše…“ vypnuté (služba pak nic nepřijímá).
|
||||
final typingProvider = StreamProvider<Map<String, Set<String>>>((ref) async* {
|
||||
final tracker = ref.watch(messagingServiceProvider).typing;
|
||||
yield tracker.current;
|
||||
yield* tracker.changes;
|
||||
});
|
||||
|
||||
/// Profilová fotka člověka podle otisku (null = iniciály). Přepočítá se,
|
||||
/// když se změní kontakty — tam dorazí nový odkaz i stažení fotky.
|
||||
final avatarProvider =
|
||||
FutureProvider.family<Uint8List?, String>((ref, fingerprint) async {
|
||||
final service = ref.watch(messagingServiceProvider);
|
||||
final me = ref.watch(identityProvider).valueOrNull;
|
||||
final contacts = ref.watch(contactsProvider).valueOrNull ?? const [];
|
||||
if (!service.isStarted) return null;
|
||||
final avatar = fingerprint == me?.id
|
||||
? service.myAvatar
|
||||
: contacts.where((c) => c.id == fingerprint).firstOrNull?.avatar;
|
||||
return avatar == null ? null : service.loadAvatar(avatar);
|
||||
});
|
||||
|
||||
/// Nastartuje službu, jakmile existuje identita. Watchovat z první
|
||||
/// obrazovky po odemčení (chat list) — do té doby se nic nespojuje.
|
||||
/// Předpokládá odemčený storage — na /chats se jde vždy až po unlocku.
|
||||
@@ -46,16 +71,21 @@ final messagingStartProvider = FutureProvider<void>((ref) async {
|
||||
|
||||
// Appka běží, ale není v popředí (jiná appka / zhasnutý displej se
|
||||
// živým procesem) → systémová notifikace. Jen jméno, žádný obsah.
|
||||
// Ztlumená konverzace notifikaci potlačí (unread badge běží dál).
|
||||
service.onMessageReceived = (contact, message) async {
|
||||
// Ztlumená konverzace notifikaci potlačí (unread badge běží dál),
|
||||
// zmínka mě ve skupině projde i tak.
|
||||
service.onMessageReceived = (contact, message, convId) async {
|
||||
final visible = WidgetsBinding.instance.lifecycleState ==
|
||||
AppLifecycleState.resumed;
|
||||
if (visible) return;
|
||||
final conv = await ref.read(storageProvider).loadConversation(contact.id);
|
||||
if (conv?.isMuted == true) return;
|
||||
final conv = await ref.read(storageProvider).loadConversation(convId);
|
||||
final mentionedMe = message.mentions.contains(identity.id);
|
||||
if (conv?.isMuted == true && !mentionedMe) return;
|
||||
PushNotifier.showMessageNotification(contact.displayName);
|
||||
};
|
||||
|
||||
service.onCallSignal = (contact, signal) =>
|
||||
ref.read(callProvider.notifier).onSignal(contact, signal);
|
||||
|
||||
await service.start(identity);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user