Модели (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
116 lines
2.9 KiB
Dart
116 lines
2.9 KiB
Dart
class PollAnswer {
|
|
final int answerId;
|
|
final String text;
|
|
final int voteCount;
|
|
final double rate;
|
|
final List<int> votes;
|
|
final bool mine;
|
|
|
|
const PollAnswer({
|
|
required this.answerId,
|
|
required this.text,
|
|
this.voteCount = 0,
|
|
this.rate = 0,
|
|
this.votes = const [],
|
|
this.mine = false,
|
|
});
|
|
}
|
|
|
|
class Poll {
|
|
final int pollId;
|
|
final String title;
|
|
final int settings;
|
|
final int version;
|
|
final int total;
|
|
final List<PollAnswer> answers;
|
|
final List<int> voterPreviewIds;
|
|
|
|
const Poll({
|
|
required this.pollId,
|
|
required this.title,
|
|
this.settings = 0,
|
|
this.version = 0,
|
|
this.total = 0,
|
|
this.answers = const [],
|
|
this.voterPreviewIds = const [],
|
|
});
|
|
|
|
bool get isMultiple => settings & 0x1 != 0;
|
|
|
|
bool get hasMyVote => answers.any((a) => a.mine);
|
|
|
|
bool votedBy(int 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'];
|
|
final stateMap = state is Map ? state : const {};
|
|
|
|
final resultsById = <int, Map>{};
|
|
final result = stateMap['result'];
|
|
if (result is List) {
|
|
for (final r in result) {
|
|
if (r is Map && r['answerId'] is int) {
|
|
resultsById[r['answerId'] as int] = r;
|
|
}
|
|
}
|
|
}
|
|
|
|
final answers = <PollAnswer>[];
|
|
final rawAnswers = map['answers'];
|
|
if (rawAnswers is List) {
|
|
for (final a in rawAnswers) {
|
|
if (a is! Map) continue;
|
|
final id = a['answerId'] as int? ?? 0;
|
|
final res = resultsById[id];
|
|
answers.add(PollAnswer(
|
|
answerId: id,
|
|
text: a['text']?.toString() ?? '',
|
|
voteCount: (res?['voteCount'] as num?)?.toInt() ?? 0,
|
|
rate: (res?['rate'] as num?)?.toDouble() ?? 0,
|
|
votes: _parseVoterIds(res?['votes']),
|
|
mine: ((res?['options'] as num?)?.toInt() ?? 0) & 0x1 != 0,
|
|
));
|
|
}
|
|
}
|
|
|
|
return Poll(
|
|
pollId: map['pollId'] as int? ?? 0,
|
|
title: map['title']?.toString() ?? '',
|
|
settings: map['settings'] as int? ?? 0,
|
|
version: map['version'] as int? ?? 0,
|
|
total: (stateMap['total'] as num?)?.toInt() ?? 0,
|
|
answers: answers,
|
|
voterPreviewIds:
|
|
(stateMap['voterPreviewIds'] as List?)?.whereType<int>().toList() ??
|
|
const [],
|
|
);
|
|
}
|
|
}
|