Модели (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
92 lines
2.2 KiB
Dart
92 lines
2.2 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
|
|
import '../api.dart';
|
|
import '../../core/protocol/opcode_map.dart';
|
|
import '../../models/poll.dart';
|
|
|
|
class PollsModule extends ChangeNotifier {
|
|
final Api _api;
|
|
|
|
PollsModule(this._api);
|
|
|
|
final Map<int, Poll> _cache = {};
|
|
final Set<int> _inFlight = {};
|
|
|
|
Poll? get(int pollId) => _cache[pollId];
|
|
|
|
Future<void> fetch(
|
|
int chatId,
|
|
String messageId,
|
|
int pollId, {
|
|
bool force = false,
|
|
}) async {
|
|
if (pollId == 0) return;
|
|
if (!force && (_cache.containsKey(pollId) || _inFlight.contains(pollId))) {
|
|
return;
|
|
}
|
|
_inFlight.add(pollId);
|
|
try {
|
|
final mid = int.tryParse(messageId) ?? 0;
|
|
final response = await _api.sendRequest(Opcode.getPollUpdates, {
|
|
'chatId': chatId,
|
|
'polls': [
|
|
{'messageId': mid, 'pollId': pollId},
|
|
],
|
|
});
|
|
if (!response.isOk) return;
|
|
|
|
final data = response.payload;
|
|
if (data is! Map) return;
|
|
|
|
final polls = data['polls'];
|
|
if (polls is! List) return;
|
|
|
|
var changed = false;
|
|
for (final p in polls) {
|
|
if (p is Map) {
|
|
final poll = Poll.fromServerMap(p);
|
|
if (poll.pollId != 0) {
|
|
_cache[poll.pollId] = poll;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
if (changed) notifyListeners();
|
|
} catch (_) {
|
|
// тихо игнорируем — опрос просто не отобразится
|
|
} finally {
|
|
_inFlight.remove(pollId);
|
|
}
|
|
}
|
|
|
|
Future<bool> vote(
|
|
int chatId,
|
|
String messageId,
|
|
int pollId,
|
|
List<int> answersIds,
|
|
) async {
|
|
try {
|
|
final response = await _api.sendRequest(Opcode.sendVote, {
|
|
'messageId': int.tryParse(messageId) ?? 0,
|
|
'chatId': chatId,
|
|
'pollId': pollId,
|
|
'answersIds': answersIds,
|
|
});
|
|
if (!response.isOk) return false;
|
|
|
|
final data = response.payload;
|
|
final state = data is Map ? data['state'] : null;
|
|
final cached = _cache[pollId];
|
|
if (state is Map && cached != null) {
|
|
_cache[pollId] = cached.withStateMap(state);
|
|
notifyListeners();
|
|
} else {
|
|
await fetch(chatId, messageId, pollId, force: true);
|
|
}
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|