feat: комментарии, немного сырые
This commit is contained in:
@@ -639,6 +639,15 @@ class ChatsModule {
|
||||
final msg = payload['message'];
|
||||
if (msg is! Map) return;
|
||||
|
||||
final msgLink = msg['link'];
|
||||
final linkPostId = (msgLink is Map) ? msgLink['postId'] : null;
|
||||
final payloadPostId = payload['postId'];
|
||||
final isCommentPush =
|
||||
payloadPostId is String ||
|
||||
(linkPostId is String) ||
|
||||
(msg['postId'] is String);
|
||||
if (isCommentPush) return;
|
||||
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
if (accountId == null) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../core/protocol/opcode_map.dart';
|
||||
import '../../core/protocol/packet.dart';
|
||||
import '../api.dart';
|
||||
import 'messages.dart' show CachedMessage;
|
||||
|
||||
class CommentsInfo {
|
||||
final String postId;
|
||||
final int? totalCount;
|
||||
final int? updatedAt;
|
||||
const CommentsInfo({
|
||||
required this.postId,
|
||||
this.totalCount,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
factory CommentsInfo.fromPayload(String postId, Map payload) {
|
||||
final raw = payload['totalCount'];
|
||||
int? count;
|
||||
if (raw is int) {
|
||||
count = raw;
|
||||
} else if (raw is String) {
|
||||
count = int.tryParse(raw);
|
||||
}
|
||||
return CommentsInfo(
|
||||
postId: postId,
|
||||
totalCount: count,
|
||||
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CommentAddedEvent {
|
||||
final int chatId;
|
||||
final String postId;
|
||||
final CachedMessage comment;
|
||||
const CommentAddedEvent(this.chatId, this.postId, this.comment);
|
||||
}
|
||||
|
||||
class CommentsModule {
|
||||
final Api _api;
|
||||
|
||||
CommentsModule(this._api);
|
||||
|
||||
final ValueNotifier<int> revision = ValueNotifier<int>(0);
|
||||
|
||||
final _infoController =
|
||||
StreamController<Map<String, CommentsInfo>>.broadcast();
|
||||
Stream<Map<String, CommentsInfo>> get infoStream => _infoController.stream;
|
||||
|
||||
final _commentController = StreamController<CommentAddedEvent>.broadcast();
|
||||
Stream<CommentAddedEvent> get commentStream => _commentController.stream;
|
||||
|
||||
Map<String, CommentsInfo> _info = <String, CommentsInfo>{};
|
||||
Map<String, CommentsInfo> get infoSnapshot => Map.unmodifiable(_info);
|
||||
|
||||
CommentsInfo? infoFor(String postId) => _info[postId];
|
||||
|
||||
int _accountId = 0;
|
||||
|
||||
void dispose() {
|
||||
_pushSub?.cancel();
|
||||
_pushSub = null;
|
||||
_infoController.close();
|
||||
_commentController.close();
|
||||
revision.dispose();
|
||||
}
|
||||
|
||||
void attachPushHandlers(Api api) {
|
||||
_pushSub?.cancel();
|
||||
_pushSub = api.pushStream.listen(_handlePush);
|
||||
}
|
||||
|
||||
StreamSubscription<Packet>? _pushSub;
|
||||
|
||||
void _handlePush(Packet packet) {
|
||||
switch (packet.opcode) {
|
||||
case Opcode.commentsInfo:
|
||||
final payload = packet.payload;
|
||||
if (payload is! Map) return;
|
||||
final updates = payload['commentsInfoUpdates'];
|
||||
if (updates is List) handleInfoUpdate(updates);
|
||||
case Opcode.notifMessage:
|
||||
_handleCommentPush(packet);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleCommentPush(Packet packet) {
|
||||
final payload = packet.payload;
|
||||
if (payload is! Map) return;
|
||||
final chatId = payload['chatId'];
|
||||
if (chatId is! int) return;
|
||||
final msg = payload['message'];
|
||||
if (msg is! Map) return;
|
||||
|
||||
final link = msg['link'];
|
||||
final postId =
|
||||
(payload['postId'] ?? (link is Map ? link['postId'] : null) ??
|
||||
msg['postId'])
|
||||
?.toString();
|
||||
if (postId == null || postId.isEmpty) return;
|
||||
|
||||
final comment = _parseComment(
|
||||
msg.cast<dynamic, dynamic>(),
|
||||
_accountId,
|
||||
chatId,
|
||||
postId,
|
||||
);
|
||||
if (comment == null) return;
|
||||
_commentController.add(CommentAddedEvent(chatId, postId, comment));
|
||||
}
|
||||
|
||||
void handleInfoUpdate(List updates) {
|
||||
if (updates.isEmpty) return;
|
||||
Map<String, CommentsInfo>? next;
|
||||
for (final raw in updates) {
|
||||
if (raw is! Map) continue;
|
||||
final postId = raw['postId']?.toString();
|
||||
final commentsInfo = raw['commentsInfo'];
|
||||
if (postId == null || commentsInfo is! Map) continue;
|
||||
final updated = CommentsInfo.fromPayload(
|
||||
postId,
|
||||
Map<String, dynamic>.from(commentsInfo.cast()),
|
||||
);
|
||||
next ??= Map<String, CommentsInfo>.from(_info);
|
||||
next[postId] = updated;
|
||||
}
|
||||
if (next == null) return;
|
||||
_info = next;
|
||||
revision.value = revision.value + 1;
|
||||
_infoController.add(Map.unmodifiable(_info));
|
||||
}
|
||||
|
||||
Future<Map<String, CommentsInfo>> fetchInfo({
|
||||
required int accountId,
|
||||
required int chatId,
|
||||
required List<String> postIds,
|
||||
}) async {
|
||||
_accountId = accountId;
|
||||
if (postIds.isEmpty) return const {};
|
||||
final response = await _api.sendRequest(Opcode.commentsInfo, {
|
||||
'chatId': chatId,
|
||||
'postIds': postIds.map((id) => int.tryParse(id) ?? id).toList(),
|
||||
});
|
||||
if (!response.isOk) return const {};
|
||||
final payload = response.payload;
|
||||
if (payload is! Map) return const {};
|
||||
final updates = payload['commentsInfoUpdates'];
|
||||
if (updates is! List) return const {};
|
||||
handleInfoUpdate(updates);
|
||||
final byPost = <String, CommentsInfo>{};
|
||||
for (final raw in updates.whereType<Map>()) {
|
||||
final postId = raw['postId']?.toString();
|
||||
final commentsInfo = raw['commentsInfo'];
|
||||
if (postId == null || commentsInfo is! Map) continue;
|
||||
byPost[postId] = CommentsInfo.fromPayload(
|
||||
postId,
|
||||
Map<String, dynamic>.from(commentsInfo.cast()),
|
||||
);
|
||||
}
|
||||
return byPost;
|
||||
}
|
||||
|
||||
Future<List<CachedMessage>> fetchHistory(
|
||||
int accountId,
|
||||
int chatId,
|
||||
String postId, {
|
||||
required int fromTime,
|
||||
int forward = 30,
|
||||
int backward = 15,
|
||||
}) async {
|
||||
_accountId = accountId;
|
||||
final payload = <String, dynamic>{
|
||||
'chatId': chatId,
|
||||
'postId': int.tryParse(postId) ?? postId,
|
||||
'from': fromTime,
|
||||
'forward': forward,
|
||||
'backward': backward,
|
||||
'getMessages': true,
|
||||
};
|
||||
|
||||
final response = await _api.sendRequest(Opcode.chatHistory, payload);
|
||||
if (!response.isOk) return const [];
|
||||
final data = response.payload;
|
||||
if (data is! Map) return const [];
|
||||
|
||||
final messagesData = data['messages'];
|
||||
if (messagesData is! List) return const [];
|
||||
|
||||
final results = <CachedMessage>[];
|
||||
for (var i = 0; i < messagesData.length; i++) {
|
||||
final m = messagesData[i];
|
||||
if (m is! Map) continue;
|
||||
final parsed = _parseComment(
|
||||
m.cast<dynamic, dynamic>(),
|
||||
accountId,
|
||||
chatId,
|
||||
postId,
|
||||
);
|
||||
if (parsed != null) results.add(parsed);
|
||||
if (i > 0 && i % 20 == 0) await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<String> sendComment(
|
||||
int accountId,
|
||||
int chatId,
|
||||
String postId,
|
||||
String text, {
|
||||
bool notify = true,
|
||||
int? replyToMessageId,
|
||||
List<Map<String, dynamic>> elements = const [],
|
||||
}) async {
|
||||
_accountId = accountId;
|
||||
final Object postIdField = int.tryParse(postId) ?? postId;
|
||||
final message = <String, dynamic>{
|
||||
'text': text,
|
||||
'cid': DateTime.now().millisecondsSinceEpoch * -1,
|
||||
'elements': elements,
|
||||
'attaches': [],
|
||||
};
|
||||
if (replyToMessageId != null) {
|
||||
message['link'] = {
|
||||
'type': 'REPLY',
|
||||
'chatId': chatId,
|
||||
'postId': postIdField,
|
||||
'messageId': replyToMessageId,
|
||||
};
|
||||
}
|
||||
final payload = <String, dynamic>{
|
||||
'chatId': chatId,
|
||||
'postId': postIdField,
|
||||
'message': message,
|
||||
'notify': notify,
|
||||
};
|
||||
|
||||
final response = await _api.sendRequest(Opcode.msgSend, payload);
|
||||
if (!response.isOk) {
|
||||
final raw = response.payload;
|
||||
final msg = (raw is Map)
|
||||
? (raw['localizedMessage'] ?? raw['message'] ?? 'Ошибка отправки')
|
||||
: 'Ошибка отправки';
|
||||
throw Exception(msg.toString());
|
||||
}
|
||||
final data = response.payload;
|
||||
if (data is Map) {
|
||||
final msgMap = data['message'];
|
||||
if (msgMap is Map) {
|
||||
final id = msgMap['id'];
|
||||
if (id != null) return id.toString();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
void sendTyping(int chatId, String postId, String type) {
|
||||
unawaited(() async {
|
||||
try {
|
||||
await _api.sendRequest(Opcode.msgTyping, {
|
||||
'chatId': chatId,
|
||||
'postId': int.tryParse(postId) ?? postId,
|
||||
'type': type,
|
||||
});
|
||||
} catch (_) {}
|
||||
}());
|
||||
}
|
||||
|
||||
CachedMessage? _parseComment(
|
||||
Map<dynamic, dynamic> m,
|
||||
int accountId,
|
||||
int chatId,
|
||||
String postId,
|
||||
) {
|
||||
final id = m['id']?.toString();
|
||||
if (id == null) return null;
|
||||
|
||||
final full = Map<String, dynamic>.from(m.cast());
|
||||
full['postId'] = postId;
|
||||
final parsed = CachedMessage.parseAttachments(full);
|
||||
final senderId = _parseIntField(m['sender']);
|
||||
|
||||
return CachedMessage(
|
||||
id: id,
|
||||
accountId: accountId,
|
||||
chatId: chatId,
|
||||
senderId: senderId,
|
||||
text: m['text']?.toString(),
|
||||
time: _parseIntField(m['time']),
|
||||
status: m['status']?.toString(),
|
||||
payload: full,
|
||||
attachments: parsed.$1,
|
||||
isControl: parsed.$2,
|
||||
);
|
||||
}
|
||||
|
||||
int _parseIntField(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
if (value is int) return value;
|
||||
if (value is String) return int.tryParse(value) ?? 0;
|
||||
return int.tryParse(value.toString()) ?? 0;
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,11 @@ abstract class Opcode {
|
||||
static const int linkInfo = 89; // Информация по ссылке / вход в канал
|
||||
static const int audioPlay = 301; // Воспроизведение аудио
|
||||
|
||||
// ── Comments (комментарии к постам каналов) ────────────────────────
|
||||
// Загрузка/отправка/набор комментариев переиспользуют chatHistory (49),
|
||||
// msgSend (64) и msgTyping (65) с добавленным полем postId.
|
||||
static const int commentsInfo = 91; // Кол-во комментариев к постам (totalCount)
|
||||
|
||||
// ── Sessions ───────────────────────────────────────────────────────
|
||||
static const int sessionsInfo = 96; // Запрос активных сессий
|
||||
static const int sessionsClose = 97; // Закрытие всех сессий
|
||||
@@ -319,6 +324,7 @@ abstract class Opcode {
|
||||
fileDownload: 'FILE_DOWNLOAD',
|
||||
linkInfo: 'LINK_INFO',
|
||||
audioPlay: 'AUDIO_PLAY',
|
||||
commentsInfo: 'COMMENTS_INFO',
|
||||
sessionsInfo: 'SESSIONS_INFO',
|
||||
sessionsClose: 'SESSIONS_CLOSE',
|
||||
phoneBindRequest: 'PHONE_BIND_REQUEST',
|
||||
|
||||
@@ -49,6 +49,10 @@ class ComposerInputBar extends StatelessWidget {
|
||||
this.channelSubscribed = true,
|
||||
this.channelSubscribing = false,
|
||||
this.onSubscribe,
|
||||
this.showStickerButton = true,
|
||||
this.showAttachButton = true,
|
||||
this.forceSend = false,
|
||||
this.hintText = 'Message',
|
||||
});
|
||||
|
||||
final String chatType;
|
||||
@@ -79,6 +83,10 @@ class ComposerInputBar extends StatelessWidget {
|
||||
final bool channelSubscribed;
|
||||
final bool channelSubscribing;
|
||||
final VoidCallback? onSubscribe;
|
||||
final bool showStickerButton;
|
||||
final bool showAttachButton;
|
||||
final bool forceSend;
|
||||
final String hintText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -207,17 +215,19 @@ class ComposerInputBar extends StatelessWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onToggleStickerPanel,
|
||||
child: Icon(
|
||||
Symbols.face,
|
||||
color: mutedIcon,
|
||||
size: 24,
|
||||
weight: 400,
|
||||
if (showStickerButton) ...[
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onToggleStickerPanel,
|
||||
child: Icon(
|
||||
Symbols.face,
|
||||
color: mutedIcon,
|
||||
size: 24,
|
||||
weight: 400,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: Focus(
|
||||
onKeyEvent: (node, event) {
|
||||
@@ -245,7 +255,7 @@ class ComposerInputBar extends StatelessWidget {
|
||||
TextAlignVertical.center,
|
||||
contextMenuBuilder: contextMenuBuilder,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Message',
|
||||
hintText: hintText,
|
||||
hintStyle: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
@@ -260,14 +270,15 @@ class ComposerInputBar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
_AttachButton(
|
||||
hasText: hasText,
|
||||
onOpen: onOpenAttach,
|
||||
onLongOpen: onOpenAttachScheduled,
|
||||
uploadStatus: uploadStatus,
|
||||
mutedIcon: mutedIcon,
|
||||
cs: cs,
|
||||
),
|
||||
if (showAttachButton)
|
||||
_AttachButton(
|
||||
hasText: hasText,
|
||||
onOpen: onOpenAttach,
|
||||
onLongOpen: onOpenAttachScheduled,
|
||||
uploadStatus: uploadStatus,
|
||||
mutedIcon: mutedIcon,
|
||||
cs: cs,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -366,7 +377,7 @@ class ComposerInputBar extends StatelessWidget {
|
||||
valueListenable: note.videoNoteMode,
|
||||
builder: (context, videoMode, _) {
|
||||
final sendMode =
|
||||
hasText || locked;
|
||||
hasText || locked || forceSend;
|
||||
final pill = _actionSurface(
|
||||
color: _flat
|
||||
? Colors.transparent
|
||||
@@ -377,14 +388,15 @@ class ComposerInputBar extends StatelessWidget {
|
||||
: _frost
|
||||
? AppFrost.inputTint(cs)
|
||||
: cs.surfaceContainerHighest,
|
||||
onTap: hasText
|
||||
onTap: (hasText || forceSend)
|
||||
? onSendText
|
||||
: locked
|
||||
? () => voiceRec.stop(
|
||||
cancel: false,
|
||||
)
|
||||
: null,
|
||||
onLongPress: hasText
|
||||
onLongPress:
|
||||
(hasText && !forceSend)
|
||||
? onScheduleMessage
|
||||
: null,
|
||||
child: SizedBox(
|
||||
@@ -421,30 +433,32 @@ class ComposerInputBar extends StatelessWidget {
|
||||
active:
|
||||
recording && !locked,
|
||||
);
|
||||
final voiceEnabled =
|
||||
!sendMode && !forceSend;
|
||||
return GestureDetector(
|
||||
onTap: sendMode
|
||||
? null
|
||||
: note.toggleMode,
|
||||
onLongPressStart: sendMode
|
||||
? null
|
||||
: (_) => videoMode
|
||||
onTap: voiceEnabled
|
||||
? note.toggleMode
|
||||
: null,
|
||||
onLongPressStart: voiceEnabled
|
||||
? (_) => videoMode
|
||||
? note.start()
|
||||
: voiceRec.start(),
|
||||
onLongPressMoveUpdate: sendMode
|
||||
? null
|
||||
: (d) => videoMode
|
||||
: voiceRec.start()
|
||||
: null,
|
||||
onLongPressMoveUpdate:
|
||||
voiceEnabled
|
||||
? (d) => videoMode
|
||||
? note.handleDrag(
|
||||
d.offsetFromOrigin,
|
||||
)
|
||||
: voiceRec.handleDrag(
|
||||
d.offsetFromOrigin,
|
||||
),
|
||||
onLongPressEnd: sendMode
|
||||
? null
|
||||
: (_) => videoMode
|
||||
)
|
||||
: null,
|
||||
onLongPressEnd: voiceEnabled
|
||||
? (_) => videoMode
|
||||
? note.handleEnd()
|
||||
: voiceRec
|
||||
.handleEnd(),
|
||||
: voiceRec.handleEnd()
|
||||
: null,
|
||||
child: visual,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:komet/backend/modules/chat_preview.dart';
|
||||
import 'package:komet/backend/modules/chats.dart';
|
||||
import 'package:komet/backend/modules/comments.dart';
|
||||
import 'package:komet/backend/modules/file_uploader.dart';
|
||||
import 'package:komet/backend/modules/upload_notification_service.dart';
|
||||
import 'package:komet/core/media/gallery_source.dart';
|
||||
@@ -201,6 +202,8 @@ class ChatScreen extends StatefulWidget {
|
||||
final ForwardRequest? forwardRequest;
|
||||
final String? initialMessageId;
|
||||
final int? initialMessageTime;
|
||||
final String? commentPostId;
|
||||
final CachedMessage? postMessage;
|
||||
|
||||
const ChatScreen({
|
||||
super.key,
|
||||
@@ -213,6 +216,8 @@ class ChatScreen extends StatefulWidget {
|
||||
this.forwardRequest,
|
||||
this.initialMessageId,
|
||||
this.initialMessageTime,
|
||||
this.commentPostId,
|
||||
this.postMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -251,6 +256,13 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
StreamSubscription<UploadEvent>? _uploadSub;
|
||||
StreamSubscription<Packet>? _pushSub;
|
||||
StreamSubscription<MessageEvent>? _messageEventSub;
|
||||
StreamSubscription<Map<String, CommentsInfo>>? _commentsInfoSub;
|
||||
StreamSubscription<CommentAddedEvent>? _commentSub;
|
||||
final Map<String, int> _commentCounts = {};
|
||||
final Set<String> _commentCountsRequested = {};
|
||||
bool get _commentsMode => widget.commentPostId != null;
|
||||
bool _commentsLoadingMore = false;
|
||||
bool _commentsHasMore = true;
|
||||
StreamSubscription<SessionState>? _connSub;
|
||||
final Map<String, ValueNotifier<Map<String, dynamic>?>> _reactionNotifiers =
|
||||
{};
|
||||
@@ -602,6 +614,16 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
_messageEventSub = chats.messageEvents
|
||||
.where((e) => e.chatId == widget.chatId)
|
||||
.listen(_onMessageEvent);
|
||||
if (_commentsMode) {
|
||||
_commentSub = commentsModule.commentStream
|
||||
.where(
|
||||
(e) =>
|
||||
e.chatId == widget.chatId && e.postId == widget.commentPostId,
|
||||
)
|
||||
.listen(_onLiveComment);
|
||||
} else if (widget.chatType == 'CHANNEL') {
|
||||
_commentsInfoSub = commentsModule.infoStream.listen(_onCommentsInfo);
|
||||
}
|
||||
ChatActivityStore.instance
|
||||
.listenable(widget.chatId)
|
||||
.addListener(_recomputeHeaderStatus);
|
||||
@@ -641,6 +663,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
|
||||
Future<void> _loadParticipantsCount() async {
|
||||
if (_commentsMode) return;
|
||||
if (widget.chatType != 'CHAT' && widget.chatType != 'CHANNEL') return;
|
||||
final info = await chats.getChatInfo(api, widget.chatId);
|
||||
if (!mounted) return;
|
||||
@@ -682,6 +705,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
if (myName.isNotEmpty) ContactCache.put(p.id, myName);
|
||||
ContactCache.putAvatar(p.id, p.baseUrl);
|
||||
}
|
||||
if (_commentsMode) return;
|
||||
_restoreDraft();
|
||||
unawaited(_loadPeerKind());
|
||||
unawaited(_loadWallpaper());
|
||||
@@ -868,6 +892,15 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
return;
|
||||
}
|
||||
if (_positioningInFlight) return;
|
||||
if (_commentsMode) {
|
||||
_initialPositionDone = true;
|
||||
_markPositioned();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || !_scrollController.hasClients) return;
|
||||
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (_messages.isEmpty) {
|
||||
if (!_hasMoreHistory) _markPositioned();
|
||||
return;
|
||||
@@ -1023,6 +1056,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
|
||||
void _updateReadMarker() {
|
||||
if (_commentsMode) return;
|
||||
if (!mounted || _myId == 0 || _messages.isEmpty) return;
|
||||
if (_awaitingPosition || !_initialPositionDone) return;
|
||||
if (!_scrollController.hasClients) return;
|
||||
@@ -1334,6 +1368,10 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
if (!mounted) return;
|
||||
_myId = activeProfile?.id ?? 0;
|
||||
}
|
||||
if (_commentsMode) {
|
||||
await _loadCommentsHistory();
|
||||
return;
|
||||
}
|
||||
if (widget.chatType == 'DIALOG') {
|
||||
unawaited(_loadOtherPresence());
|
||||
}
|
||||
@@ -1358,6 +1396,14 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
if (!_scrollController.hasClients) return;
|
||||
if (_historyAutoloadSuppressed) return;
|
||||
if (_isLoading) return;
|
||||
if (_commentsMode) {
|
||||
if (_commentsLoadingMore || !_commentsHasMore || _messages.isEmpty) return;
|
||||
final pos = _scrollController.position;
|
||||
if (pos.pixels - pos.minScrollExtent <= _historyPrefetchExtent) {
|
||||
unawaited(_loadMoreComments());
|
||||
}
|
||||
return;
|
||||
}
|
||||
_maybeFillGap();
|
||||
if (_isLoadingMore || !_hasMoreHistory) return;
|
||||
if (_messages.isEmpty) return;
|
||||
@@ -1518,9 +1564,158 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
_pruneReactionNotifiers();
|
||||
_chatController.persistSessionCache();
|
||||
_reapplyPinIfNeeded();
|
||||
_requestCommentCounts();
|
||||
}
|
||||
}
|
||||
|
||||
void _requestCommentCounts() {
|
||||
if (_commentsMode) return;
|
||||
if ((chat?.type ?? widget.chatType) != 'CHANNEL') return;
|
||||
final pending = <String>[];
|
||||
for (final m in _messages) {
|
||||
if (m.isControl) continue;
|
||||
if (_commentCountsRequested.contains(m.id)) continue;
|
||||
_commentCountsRequested.add(m.id);
|
||||
pending.add(m.id);
|
||||
}
|
||||
if (pending.isEmpty) return;
|
||||
unawaited(
|
||||
commentsModule.fetchInfo(
|
||||
accountId: _myId,
|
||||
chatId: widget.chatId,
|
||||
postIds: pending,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onCommentsInfo(Map<String, CommentsInfo> info) {
|
||||
if (!mounted) return;
|
||||
var changed = false;
|
||||
for (final m in _messages) {
|
||||
final count = info[m.id]?.totalCount;
|
||||
if (count == null) continue;
|
||||
if (_commentCounts[m.id] != count) {
|
||||
_commentCounts[m.id] = count;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) setState(() {});
|
||||
}
|
||||
|
||||
String _commentsLabelFor(String postId) {
|
||||
final count = _commentCounts[postId];
|
||||
if (count == null || count == 0) return 'Комментировать';
|
||||
if (count == 1) return '1 комментарий';
|
||||
if (count % 10 == 1 && count % 100 != 11) return '$count комментарий';
|
||||
return '$count комментариев';
|
||||
}
|
||||
|
||||
void _openComments(CachedMessage post) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ChatScreen(
|
||||
chatId: widget.chatId,
|
||||
name: widget.name,
|
||||
imageUrl: widget.imageUrl,
|
||||
chatType: 'CHANNEL',
|
||||
commentPostId: post.id,
|
||||
postMessage: _stripInlineKeyboard(post),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
CachedMessage _stripInlineKeyboard(CachedMessage post) {
|
||||
final attaches = post.attachments;
|
||||
if (attaches == null || attaches.isEmpty) return post;
|
||||
final filtered = attaches
|
||||
.where((a) => a.type != AttachmentType.inlineKeyboard)
|
||||
.toList();
|
||||
if (filtered.length == attaches.length) return post;
|
||||
return post.copyWith(attachments: filtered);
|
||||
}
|
||||
|
||||
Future<void> _loadCommentsHistory() async {
|
||||
final post = widget.postMessage;
|
||||
final loaded = await commentsModule.fetchHistory(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
widget.commentPostId!,
|
||||
fromTime: post?.time ?? DateTime.now().millisecondsSinceEpoch,
|
||||
forward: 30,
|
||||
backward: 0,
|
||||
);
|
||||
if (!mounted) return;
|
||||
final comments = [...loaded]..sort((a, b) => a.time.compareTo(b.time));
|
||||
_messages = post != null ? [post, ...comments] : comments;
|
||||
_commentsHasMore = comments.isNotEmpty;
|
||||
_syncReactionNotifiersFromMessages();
|
||||
unawaited(_resolveCommentNames(comments));
|
||||
_bumpMessages();
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_onLoadingFinished();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadMoreComments() async {
|
||||
if (_commentsLoadingMore || !_commentsHasMore || _messages.isEmpty) return;
|
||||
_commentsLoadingMore = true;
|
||||
final newest = _messages.last;
|
||||
try {
|
||||
final more = await commentsModule.fetchHistory(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
widget.commentPostId!,
|
||||
fromTime: newest.time,
|
||||
forward: 30,
|
||||
backward: 0,
|
||||
);
|
||||
if (!mounted) return;
|
||||
final existing = _messages.map((m) => m.id).toSet();
|
||||
final fresh = more.where((c) => !existing.contains(c.id)).toList();
|
||||
if (fresh.isEmpty) {
|
||||
_commentsHasMore = false;
|
||||
} else {
|
||||
_messages = [..._messages, ...fresh]
|
||||
..sort((a, b) => a.time.compareTo(b.time));
|
||||
_syncReactionNotifiersFromMessages();
|
||||
unawaited(_resolveCommentNames(fresh));
|
||||
_bumpMessages();
|
||||
}
|
||||
} finally {
|
||||
_commentsLoadingMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _onLiveComment(CommentAddedEvent event) {
|
||||
if (!mounted) return;
|
||||
final comment = event.comment;
|
||||
if (comment.senderId == _myId) return;
|
||||
if (_messages.any((m) => m.id == comment.id)) return;
|
||||
final nearBottom = _isNearListBottom();
|
||||
_messages.add(comment);
|
||||
_syncReactionNotifiersFromMessages();
|
||||
_bumpMessages();
|
||||
unawaited(_resolveCommentNames([comment]));
|
||||
if (nearBottom) _scrollToBottom();
|
||||
}
|
||||
|
||||
bool _isNearListBottom() {
|
||||
if (!_scrollController.hasClients) return true;
|
||||
return _scrollController.position.pixels <= _historyPrefetchExtent;
|
||||
}
|
||||
|
||||
Future<void> _resolveCommentNames(List<CachedMessage> list) async {
|
||||
final ids = list
|
||||
.map((m) => m.senderId)
|
||||
.where((id) => id != 0 && ContactCache.get(id) == null)
|
||||
.toSet();
|
||||
if (ids.isEmpty) return;
|
||||
final resolved = await messagesModule.ensureContactNames(ids);
|
||||
if (resolved && mounted) _bumpMessages();
|
||||
}
|
||||
|
||||
void _syncReactionNotifiersFromMessages() {
|
||||
for (final m in _messages) {
|
||||
if (_reactionNotifiers.containsKey(m.id)) continue;
|
||||
@@ -1601,6 +1796,8 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
_uploadSub?.cancel();
|
||||
_pushSub?.cancel();
|
||||
_messageEventSub?.cancel();
|
||||
_commentsInfoSub?.cancel();
|
||||
_commentSub?.cancel();
|
||||
_connSub?.cancel();
|
||||
_voiceRec.dispose();
|
||||
_note.dispose();
|
||||
@@ -1668,7 +1865,9 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
|
||||
void _restoreDraft() {
|
||||
if (_myId == 0 || _messageController.text.isNotEmpty) return;
|
||||
if (_myId == 0 || _commentsMode || _messageController.text.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
final draft = DraftStore.instance.get(_myId, widget.chatId);
|
||||
if (draft == null || draft.isEmpty) return;
|
||||
_messageController.text = draft;
|
||||
@@ -1678,7 +1877,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
|
||||
void _saveDraft() {
|
||||
if (_myId == 0) return;
|
||||
if (_myId == 0 || _commentsMode) return;
|
||||
unawaited(
|
||||
DraftStore.instance.set(
|
||||
_myId,
|
||||
@@ -2146,7 +2345,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
},
|
||||
),
|
||||
ComposerInputBar(
|
||||
chatType: widget.chatType,
|
||||
chatType: _commentsMode ? 'CHAT' : widget.chatType,
|
||||
chrome: _effectiveChrome,
|
||||
style: AppComposerStyle.current.value,
|
||||
background: AppComposerBackground.current.value,
|
||||
@@ -2175,6 +2374,10 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
channelSubscribed: !_previewChat,
|
||||
channelSubscribing: _subscribing,
|
||||
onSubscribe: _subscribeChannel,
|
||||
showStickerButton: !_commentsMode,
|
||||
showAttachButton: !_commentsMode,
|
||||
forceSend: _commentsMode,
|
||||
hintText: _commentsMode ? 'Комментарий' : 'Message',
|
||||
),
|
||||
StickerPanelView(
|
||||
stickers: _stickers,
|
||||
@@ -2502,6 +2705,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
|
||||
void _onMessageEvent(MessageEvent event) {
|
||||
if (!mounted) return;
|
||||
if (_commentsMode) return;
|
||||
switch (event) {
|
||||
case MessageAddedEvent(:final message):
|
||||
if (message.senderId == _myId) return;
|
||||
@@ -2662,7 +2866,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
cs: cs,
|
||||
embedded: widget.embedded,
|
||||
chatId: widget.chatId,
|
||||
name: widget.name,
|
||||
name: _commentsMode ? 'Комментарии' : widget.name,
|
||||
imageUrl: widget.imageUrl,
|
||||
chatType: widget.chatType,
|
||||
isOfficial: chat?.isOfficial ?? false,
|
||||
@@ -2670,13 +2874,14 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
headerStatus: _headerStatusNotifier,
|
||||
scheduledCount: _scheduledCount,
|
||||
otherUnread: _otherUnread,
|
||||
showCall:
|
||||
widget.chatType == 'DIALOG' && !_peerIsBot,
|
||||
showCall: !_commentsMode &&
|
||||
widget.chatType == 'DIALOG' &&
|
||||
!_peerIsBot,
|
||||
onClose: widget.onClose,
|
||||
onOpenInfo: _openChatInfo,
|
||||
onOpenInfo: _commentsMode ? () {} : _openChatInfo,
|
||||
onOpenScheduled: _openScheduledMessages,
|
||||
onCall: _startCall,
|
||||
onMenu: _openChatMenu,
|
||||
onMenu: _commentsMode ? (_) {} : _openChatMenu,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -3034,6 +3239,10 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
|
||||
void _recomputeHeaderStatus() {
|
||||
if (_commentsMode) {
|
||||
_headerStatusNotifier.value = '';
|
||||
return;
|
||||
}
|
||||
_headerStatusNotifier.value = _headerStatus();
|
||||
}
|
||||
|
||||
@@ -3249,22 +3458,25 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
_lastSentId = tempId;
|
||||
_messages.add(composed);
|
||||
_messageController.clear();
|
||||
if (DraftStore.instance.get(_myId, widget.chatId) != null) {
|
||||
if (!_commentsMode &&
|
||||
DraftStore.instance.get(_myId, widget.chatId) != null) {
|
||||
unawaited(DraftStore.instance.clear(_myId, widget.chatId));
|
||||
}
|
||||
_bumpMessages();
|
||||
unawaited(_persistOutgoing(composed));
|
||||
unawaited(
|
||||
chats.applyOutgoing(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
messageId: tempId,
|
||||
time: now,
|
||||
text: text,
|
||||
status: composed.status ?? 'sending',
|
||||
elements: elements,
|
||||
),
|
||||
);
|
||||
if (!_commentsMode) {
|
||||
unawaited(_persistOutgoing(composed));
|
||||
unawaited(
|
||||
chats.applyOutgoing(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
messageId: tempId,
|
||||
time: now,
|
||||
text: text,
|
||||
status: composed.status ?? 'sending',
|
||||
elements: elements,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Instant tactile "whoosh" the moment the message leaves the composer,
|
||||
// not after the network round-trip — feedback must feel immediate.
|
||||
@@ -3276,13 +3488,22 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
if (!online) return;
|
||||
|
||||
try {
|
||||
final actualId = await messagesModule.sendMessage(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
text,
|
||||
replyToMessageId: replyId,
|
||||
elements: elements,
|
||||
);
|
||||
final actualId = _commentsMode
|
||||
? await commentsModule.sendComment(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
widget.commentPostId!,
|
||||
text,
|
||||
replyToMessageId: replyId,
|
||||
elements: elements,
|
||||
)
|
||||
: await messagesModule.sendMessage(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
text,
|
||||
replyToMessageId: replyId,
|
||||
elements: elements,
|
||||
);
|
||||
|
||||
final index = _messages.indexWhere((m) => m.id == tempId);
|
||||
if (index != -1 && mounted) {
|
||||
@@ -3298,21 +3519,23 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
);
|
||||
_messages[index] = sent;
|
||||
_bumpMessages();
|
||||
unawaited(_persistOutgoing(sent, removeId: tempId));
|
||||
unawaited(
|
||||
chats.applyOutgoing(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
messageId: sent.id,
|
||||
time: now,
|
||||
text: text,
|
||||
status: 'sent',
|
||||
elements: elements,
|
||||
),
|
||||
);
|
||||
if (!_commentsMode) {
|
||||
unawaited(_persistOutgoing(sent, removeId: tempId));
|
||||
unawaited(
|
||||
chats.applyOutgoing(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
messageId: sent.id,
|
||||
time: now,
|
||||
text: text,
|
||||
status: 'sent',
|
||||
elements: elements,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (chat == null) {
|
||||
if (!_commentsMode && chat == null) {
|
||||
unawaited(
|
||||
chats.refreshChats(api, [widget.chatId]).then((list) {
|
||||
if (!mounted || list.isEmpty) return;
|
||||
@@ -3337,18 +3560,20 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
);
|
||||
_messages[index] = queued;
|
||||
_bumpMessages();
|
||||
unawaited(_persistOutgoing(queued));
|
||||
unawaited(
|
||||
chats.applyOutgoing(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
messageId: tempId,
|
||||
time: now,
|
||||
text: text,
|
||||
status: 'pending',
|
||||
elements: elements,
|
||||
),
|
||||
);
|
||||
if (!_commentsMode) {
|
||||
unawaited(_persistOutgoing(queued));
|
||||
unawaited(
|
||||
chats.applyOutgoing(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
messageId: tempId,
|
||||
time: now,
|
||||
text: text,
|
||||
status: 'pending',
|
||||
elements: elements,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4733,13 +4958,20 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
? _messages[msgIndex + 1]
|
||||
: null;
|
||||
|
||||
final bool isChannelPost =
|
||||
!_commentsMode &&
|
||||
(chat?.type ?? widget.chatType) == 'CHANNEL' &&
|
||||
!message.isControl;
|
||||
|
||||
final bubble = MessageBubble(
|
||||
message: message,
|
||||
isMe: isMe,
|
||||
myId: _myId,
|
||||
prevMessage: prevMessage,
|
||||
nextMessage: nextMessage,
|
||||
chatType: chat?.type ?? 'CHAT',
|
||||
chatType: _commentsMode
|
||||
? 'CHAT'
|
||||
: (chat?.type ?? 'CHAT'),
|
||||
chatId: widget.chatId,
|
||||
photoActions: _photoActions,
|
||||
overrideStatus: _effectiveStatus(message),
|
||||
@@ -4760,6 +4992,12 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
peerAvatarUrl: widget.imageUrl,
|
||||
textSelection: _textSelection,
|
||||
onExitTextSelection: _exitTextSelection,
|
||||
commentsLabel: isChannelPost
|
||||
? _commentsLabelFor(message.id)
|
||||
: null,
|
||||
onCommentsTap: isChannelPost
|
||||
? () => _openComments(message)
|
||||
: null,
|
||||
);
|
||||
|
||||
final canReport = !isMe && !message.isControl;
|
||||
@@ -6643,3 +6881,4 @@ class _ChatMessageListState extends State<_ChatMessageList> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ void showMessageActions({
|
||||
Future<List<({int id, String title})>> Function()? loadReportReasons,
|
||||
Future<bool> Function(int reasonId)? onReport,
|
||||
VoidCallback? onDelete,
|
||||
bool allowDelete = true,
|
||||
VoidCallback? onEdit,
|
||||
VoidCallback? onReply,
|
||||
VoidCallback? onForward,
|
||||
@@ -151,6 +152,7 @@ void showMessageActions({
|
||||
loadReportReasons: loadReportReasons,
|
||||
onReport: onReport,
|
||||
onDelete: onDelete,
|
||||
allowDelete: allowDelete,
|
||||
onEdit: onEdit,
|
||||
onReply: onReply,
|
||||
onForward: onForward,
|
||||
@@ -186,6 +188,7 @@ class _MessageActionsLayer extends StatefulWidget {
|
||||
final Future<List<({int id, String title})>> Function()? loadReportReasons;
|
||||
final Future<bool> Function(int reasonId)? onReport;
|
||||
final VoidCallback? onDelete;
|
||||
final bool allowDelete;
|
||||
final VoidCallback? onEdit;
|
||||
final VoidCallback? onReply;
|
||||
final VoidCallback? onForward;
|
||||
@@ -213,6 +216,7 @@ class _MessageActionsLayer extends StatefulWidget {
|
||||
this.loadReportReasons,
|
||||
this.onReport,
|
||||
this.onDelete,
|
||||
this.allowDelete = true,
|
||||
this.onEdit,
|
||||
this.onReply,
|
||||
this.onForward,
|
||||
@@ -525,12 +529,13 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
|
||||
_showReportView,
|
||||
destructive: true,
|
||||
),
|
||||
_Action(
|
||||
Symbols.delete,
|
||||
l10n.msgActionsDelete,
|
||||
_delete,
|
||||
destructive: true,
|
||||
),
|
||||
if (widget.allowDelete)
|
||||
_Action(
|
||||
Symbols.delete,
|
||||
l10n.msgActionsDelete,
|
||||
_delete,
|
||||
destructive: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -203,6 +203,8 @@ class MessageBubble extends StatelessWidget {
|
||||
final String? peerAvatarUrl;
|
||||
final ValueListenable<({String id, Offset pos})?>? textSelection;
|
||||
final VoidCallback? onExitTextSelection;
|
||||
final String? commentsLabel;
|
||||
final VoidCallback? onCommentsTap;
|
||||
|
||||
const MessageBubble({
|
||||
super.key,
|
||||
@@ -226,6 +228,8 @@ class MessageBubble extends StatelessWidget {
|
||||
this.peerAvatarUrl,
|
||||
this.textSelection,
|
||||
this.onExitTextSelection,
|
||||
this.commentsLabel,
|
||||
this.onCommentsTap,
|
||||
});
|
||||
|
||||
bool _computeHasPhotoWithCaption() {
|
||||
@@ -619,6 +623,29 @@ class MessageBubble extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
final bool hasCommentsFooter = onCommentsTap != null;
|
||||
final EdgeInsets containerPadding = hasCommentsFooter
|
||||
? EdgeInsets.zero
|
||||
: padding;
|
||||
|
||||
final Widget innerContent = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showSenderName)
|
||||
_buildSenderHeader(cs, padding == EdgeInsets.zero),
|
||||
withReply(
|
||||
reactionsInside
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [bubbleContent, _reactionsBar(cs)],
|
||||
)
|
||||
: bubbleContent,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final Widget bubbleBox = ListenableBuilder(
|
||||
listenable: Listenable.merge([
|
||||
AppBubbleShape.current,
|
||||
@@ -638,26 +665,24 @@ class MessageBubble extends StatelessWidget {
|
||||
hasMultiPhotos,
|
||||
),
|
||||
),
|
||||
padding: padding,
|
||||
padding: containerPadding,
|
||||
child: child,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showSenderName)
|
||||
_buildSenderHeader(cs, padding == EdgeInsets.zero),
|
||||
withReply(
|
||||
reactionsInside
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [bubbleContent, _reactionsBar(cs)],
|
||||
)
|
||||
: bubbleContent,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: hasCommentsFooter
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: padding == EdgeInsets.zero
|
||||
? const EdgeInsets.symmetric(horizontal: 14, vertical: 10)
|
||||
: padding,
|
||||
child: innerContent,
|
||||
),
|
||||
_buildCommentsFooter(cs),
|
||||
],
|
||||
)
|
||||
: innerContent,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
@@ -703,6 +728,54 @@ class MessageBubble extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCommentsFooter(ColorScheme cs) {
|
||||
final label = commentsLabel ?? 'Комментарии';
|
||||
final accent = isMe ? cs.onPrimaryContainer : cs.primary;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onCommentsTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Divider(
|
||||
height: 0.5,
|
||||
thickness: 0.5,
|
||||
color: cs.onSurfaceVariant.withValues(alpha: 0.18),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 11,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.mode_comment, size: 19, color: accent),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: accent,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Symbols.chevron_right,
|
||||
size: 20,
|
||||
color: cs.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInlineKeyboard(
|
||||
BuildContext context,
|
||||
ColorScheme cs,
|
||||
|
||||
@@ -50,6 +50,7 @@ import 'core/config/app_theme_schedule.dart';
|
||||
import 'core/config/app_digital_id_mode.dart';
|
||||
import 'backend/modules/account.dart';
|
||||
import 'backend/modules/chats.dart';
|
||||
import 'backend/modules/comments.dart';
|
||||
import 'backend/modules/contacts.dart';
|
||||
import 'backend/modules/file_uploader.dart';
|
||||
import 'backend/modules/messages.dart';
|
||||
@@ -86,6 +87,7 @@ import 'frontend/widgets/theme_reveal.dart';
|
||||
final api = Api();
|
||||
final accountModule = AccountModule(api);
|
||||
final messagesModule = MessagesModule(api);
|
||||
final commentsModule = CommentsModule(api);
|
||||
final sharedContentModule = SharedContentModule(api);
|
||||
final pollsModule = PollsModule(api);
|
||||
final stickersModule = StickersModule(api);
|
||||
@@ -183,6 +185,7 @@ void main(List<String> args) async {
|
||||
}
|
||||
attachInfoCacheApi(api);
|
||||
chats.attachGlobalPushHandlers(api);
|
||||
commentsModule.attachPushHandlers(api);
|
||||
storiesModule.attach();
|
||||
unawaited(storiesModule.loadCache());
|
||||
unawaited(DeepLinkService.instance.init());
|
||||
|
||||
Reference in New Issue
Block a user