feat: полное отображение типов сообщений + предпросмотр ссылок и оптимизация фото
Модели (lib/models/):
- attachment.dart — новые типы ShareAttachment (OG-карточка ссылки) и
CallAttachment (звонок); FILE получил вложенное превью и поле token;
LOCATION — поле zoom; единый декодер previewData (сырой WebP → data-URI)
вынесен в decodeAttachPreview
- poll.dart — корректный парсинг голосов ({userId,timestamp}) и признак
своего голоса (options & 1); helper withStateMap
Лента сообщений (message_bubble.dart):
- SHARE — карточка предпросмотра ссылки (картинка, домен, заголовок,
описание), тап открывает URL
- CALL — отображение звонков: аудио/видео/групповые/пропущенные,
длительность, направление; симметричная иконка
- LOCATION — карточка геопозиции, тап открывает карту
- FILE — превью-картинка над строкой файла
- кликабельные ссылки в тексте и подписях (LinkText)
- фикс лишней ширины бабблов гео/share/опроса (IntrinsicWidth + stretch)
- оптимизация рендера фото: убран fade-in, уменьшен размер декода
плиток фото-сетки
Опросы:
- интерактивное голосование через opcode 304 (polls.dart, poll_view.dart):
одиночный/множественный выбор, отметка своего ответа, проценты
Список чатов (chats.dart):
- превью типа вложения для сообщений без текста («Фото», «Опрос: …»,
«Звонок», «Геопозиция», «Файл: …», «Ссылка: …» и т.д.)
Настройки разработчика:
- тумблер «Предпросмотр ссылок» (app_link_preview.dart) — отключает
SHARE-карточки, оставляя текст со ссылкой; применяется на лету
Прочее:
- зависимость url_launcher ^6.3.1 (open_external_url / open_location_on_map)
- bump версии 0.5.0+11 → 0.5.0+12
This commit is contained in:
+33
-5
@@ -4,6 +4,7 @@ class PollAnswer {
|
||||
final int voteCount;
|
||||
final double rate;
|
||||
final List<int> votes;
|
||||
final bool mine;
|
||||
|
||||
const PollAnswer({
|
||||
required this.answerId,
|
||||
@@ -11,6 +12,7 @@ class PollAnswer {
|
||||
this.voteCount = 0,
|
||||
this.rate = 0,
|
||||
this.votes = const [],
|
||||
this.mine = false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,8 +37,36 @@ class Poll {
|
||||
|
||||
bool get isMultiple => settings & 0x1 != 0;
|
||||
|
||||
bool get hasMyVote => answers.any((a) => a.mine);
|
||||
|
||||
bool votedBy(int userId) =>
|
||||
answers.any((a) => a.votes.contains(userId));
|
||||
answers.any((a) => a.mine || a.votes.contains(userId));
|
||||
|
||||
static List<int> _parseVoterIds(dynamic votes) {
|
||||
if (votes is! List) return const [];
|
||||
final ids = <int>[];
|
||||
for (final v in votes) {
|
||||
if (v is int) {
|
||||
ids.add(v);
|
||||
} else if (v is Map && v['userId'] is int) {
|
||||
ids.add(v['userId'] as int);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
Poll withStateMap(Map<dynamic, dynamic> stateMap) {
|
||||
return Poll.fromServerMap({
|
||||
'pollId': pollId,
|
||||
'title': title,
|
||||
'settings': settings,
|
||||
'version': version,
|
||||
'answers': [
|
||||
for (final a in answers) {'answerId': a.answerId, 'text': a.text},
|
||||
],
|
||||
'state': stateMap,
|
||||
});
|
||||
}
|
||||
|
||||
factory Poll.fromServerMap(Map<dynamic, dynamic> map) {
|
||||
final state = map['state'];
|
||||
@@ -64,10 +94,8 @@ class Poll {
|
||||
text: a['text']?.toString() ?? '',
|
||||
voteCount: (res?['voteCount'] as num?)?.toInt() ?? 0,
|
||||
rate: (res?['rate'] as num?)?.toDouble() ?? 0,
|
||||
votes: (res?['votes'] as List?)
|
||||
?.whereType<int>()
|
||||
.toList() ??
|
||||
const [],
|
||||
votes: _parseVoterIds(res?['votes']),
|
||||
mine: ((res?['options'] as num?)?.toInt() ?? 0) & 0x1 != 0,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user