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:
Julius Joska
2026-10-03 22:44:53 +02:00
parent 933a74c900
commit 5246a97db2
56 changed files with 12028 additions and 1668 deletions
+40
View File
@@ -0,0 +1,40 @@
/// Porovnávání textu pro hledání: bez ohledu na velikost písmen a diakritiku.
/// Lidé na mobilu často píšou „prectu“ místo „přečtu“ — hledání to má najít.
library;
const _map = {
'á': 'a', 'ä': 'a', 'č': 'c', 'ď': 'd', 'é': 'e', 'ě': 'e', 'ë': 'e',
'í': 'i', 'ľ': 'l', 'ĺ': 'l', 'ň': 'n', 'ó': 'o', 'ô': 'o', 'ö': 'o',
'ř': 'r', 'ŕ': 'r', 'š': 's', 'ť': 't', 'ú': 'u', 'ů': 'u', 'ü': 'u',
'ý': 'y', 'ž': 'z', 'ß': 'ss',
};
/// Malá písmena bez diakritiky. Délka se může změnit jen u „ß“, proto
/// pozice shod počítej přes [foldedIndexes], ne přes indexy výsledku.
String fold(String input) {
final out = StringBuffer();
for (final ch in input.toLowerCase().split('')) {
out.write(_map[ch] ?? ch);
}
return out.toString();
}
/// Začátky všech výskytů [query] v [text] (indexy do PŮVODNÍHO textu).
/// Znaky se skládají po jednom, takže indexy sedí i pro zvýraznění.
List<int> foldedIndexes(String text, String query) {
final q = fold(query);
if (q.isEmpty) return const [];
final chars = text.split('');
final folded = chars.map(fold).toList();
final hits = <int>[];
for (var i = 0; i < chars.length; i++) {
var j = i;
var matched = '';
while (j < chars.length && matched.length < q.length) {
matched += folded[j];
j++;
}
if (matched.startsWith(q)) hits.add(i);
}
return hits;
}