mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-20 17:35:34 +10:00
update to current dev a50c0d0b2d
This commit is contained in:
@@ -9,6 +9,8 @@ import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../models/community.dart';
|
||||
import '../storage/community_store.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../helpers/chat_scroll_controller.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
@@ -57,8 +59,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final ChatScrollController _scrollController = ChatScrollController();
|
||||
final FocusNode _textFieldFocusNode = FocusNode();
|
||||
ChannelMessage? _replyingToMessage;
|
||||
final CommunityStore _communityStore = CommunityStore();
|
||||
final CommunityPskIndex _communityIndex = CommunityPskIndex();
|
||||
final Map<String, GlobalKey> _messageKeys = {};
|
||||
bool _isLoadingOlder = false;
|
||||
bool _communitiesLoaded = false;
|
||||
|
||||
MeshCoreConnector? _connector;
|
||||
DateTime? _lastChannelSendAt;
|
||||
@@ -82,6 +87,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final idx = widget.channel.index;
|
||||
final unread = widget.initialUnreadCount;
|
||||
final messages = connector.getChannelMessages(widget.channel);
|
||||
_loadCommunities();
|
||||
ChannelMessage? anchor;
|
||||
if (unread > 0) {
|
||||
anchor = _findOldestUnreadChannelAnchor(messages, unread);
|
||||
@@ -108,6 +114,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Reload communities when returning from another screen
|
||||
Future<void> _loadCommunities() async {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
final communities = await _communityStore.loadCommunities();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_communityIndex.initialize(communities);
|
||||
_communitiesLoaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ChannelMessage? _findOldestUnreadChannelAnchor(
|
||||
List<ChannelMessage> messages,
|
||||
int unreadCount,
|
||||
@@ -194,16 +213,63 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _channelIcon(Channel channel) {
|
||||
// Determine icon based on channel type
|
||||
final ChannelType channelType = Channel.getChannelType(
|
||||
channel,
|
||||
_communityIndex,
|
||||
);
|
||||
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
|
||||
IconData icon;
|
||||
switch (channelType) {
|
||||
case ChannelType.communityPublic:
|
||||
icon = Icons.groups;
|
||||
case ChannelType.communityHashtag:
|
||||
icon = Icons.tag;
|
||||
case ChannelType.public:
|
||||
icon = Icons.public;
|
||||
case ChannelType.hashtag:
|
||||
icon = Icons.tag;
|
||||
case ChannelType.private:
|
||||
icon = Icons.lock;
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: _communitiesLoaded
|
||||
? Icon(icon, size: 20)
|
||||
: SizedBox.square(dimension: 20),
|
||||
),
|
||||
if (isCommunityChannel)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).cardColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: const Icon(Icons.people, size: 8, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
widget.channel.isPublicChannel ? Icons.public : Icons.tag,
|
||||
size: 20,
|
||||
),
|
||||
_channelIcon(widget.channel),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -1321,11 +1387,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
}
|
||||
|
||||
void _showMessageActions(ChannelMessage message) {
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final translationService = context.read<TranslationService>();
|
||||
final canTranslateMessage =
|
||||
settings.translationEnabled &&
|
||||
!settings.autoTranslateIncomingMessages &&
|
||||
translationService.canTranslateIncoming(
|
||||
text: message.text,
|
||||
isCli: false,
|
||||
|
||||
@@ -44,11 +44,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
with DisconnectNavigationMixin {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final CommunityStore _communityStore = CommunityStore();
|
||||
Timer? _searchDebounce;
|
||||
final CommunityPskIndex _communityIndex = CommunityPskIndex();
|
||||
List<Community> _communities = [];
|
||||
|
||||
// Cache of PSK hex -> Community for quick lookup
|
||||
final Map<String, Community> _pskToCommunity = {};
|
||||
Timer? _searchDebounce;
|
||||
|
||||
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
|
||||
|
||||
@@ -71,37 +69,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_communities = communities;
|
||||
_buildPskCommunityMap();
|
||||
_communityIndex.initialize(communities);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _buildPskCommunityMap() {
|
||||
_pskToCommunity.clear();
|
||||
for (final community in _communities) {
|
||||
// Map the community public channel PSK
|
||||
final publicPsk = community.deriveCommunityPublicPsk();
|
||||
_pskToCommunity[Channel.formatPskHex(publicPsk)] = community;
|
||||
|
||||
// Map all known hashtag channel PSKs
|
||||
for (final hashtag in community.hashtagChannels) {
|
||||
final hashtagPsk = community.deriveCommunityHashtagPsk(hashtag);
|
||||
_pskToCommunity[Channel.formatPskHex(hashtagPsk)] = community;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the community this channel belongs to, or null if not a community channel
|
||||
Community? _getCommunityForChannel(Channel channel) {
|
||||
return _pskToCommunity[channel.pskHex];
|
||||
}
|
||||
|
||||
/// Returns true if this is the community's public channel
|
||||
bool _isCommunityPublicChannel(Channel channel, Community community) {
|
||||
final publicPsk = community.deriveCommunityPublicPsk();
|
||||
return channel.pskHex == Channel.formatPskHex(publicPsk);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchDebounce?.cancel();
|
||||
@@ -360,6 +332,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
selectedIndex: 1,
|
||||
onDestinationSelected: (index) =>
|
||||
_handleQuickSwitch(index, context),
|
||||
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
|
||||
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -375,37 +349,37 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
int? dragIndex,
|
||||
}) {
|
||||
final unreadCount = connector.getUnreadCountForChannel(channel);
|
||||
final community = _getCommunityForChannel(channel);
|
||||
final isCommunityChannel = community != null;
|
||||
final isCommunityPublic =
|
||||
isCommunityChannel && _isCommunityPublicChannel(channel, community);
|
||||
|
||||
// Determine icon and colors based on channel type
|
||||
IconData icon;
|
||||
Color iconColor;
|
||||
Color bgColor;
|
||||
|
||||
if (isCommunityChannel) {
|
||||
// Community channel styling
|
||||
iconColor = Colors.purple;
|
||||
bgColor = Colors.purple.withValues(alpha: 0.2);
|
||||
if (isCommunityPublic) {
|
||||
final ChannelType channelType = Channel.getChannelType(
|
||||
channel,
|
||||
_communityIndex,
|
||||
);
|
||||
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
|
||||
switch (channelType) {
|
||||
case ChannelType.communityPublic:
|
||||
icon = Icons.groups;
|
||||
} else {
|
||||
iconColor = Colors.purple;
|
||||
bgColor = Colors.purple.withValues(alpha: 0.2);
|
||||
case ChannelType.communityHashtag:
|
||||
icon = Icons.tag;
|
||||
}
|
||||
} else if (channel.isPublicChannel) {
|
||||
icon = Icons.public;
|
||||
iconColor = Colors.green;
|
||||
bgColor = Colors.green.withValues(alpha: 0.2);
|
||||
} else if (channel.name.startsWith('#')) {
|
||||
icon = Icons.tag;
|
||||
iconColor = Colors.blue;
|
||||
bgColor = Colors.blue.withValues(alpha: 0.2);
|
||||
} else {
|
||||
icon = Icons.lock;
|
||||
iconColor = Colors.blue;
|
||||
bgColor = Colors.blue.withValues(alpha: 0.2);
|
||||
iconColor = Colors.purple;
|
||||
bgColor = Colors.purple.withValues(alpha: 0.2);
|
||||
case ChannelType.public:
|
||||
icon = Icons.public;
|
||||
iconColor = Colors.green;
|
||||
bgColor = Colors.green.withValues(alpha: 0.2);
|
||||
case ChannelType.hashtag:
|
||||
icon = Icons.tag;
|
||||
iconColor = Colors.blue;
|
||||
bgColor = Colors.blue.withValues(alpha: 0.2);
|
||||
case ChannelType.private:
|
||||
icon = Icons.lock;
|
||||
iconColor = Colors.blue;
|
||||
bgColor = Colors.blue.withValues(alpha: 0.2);
|
||||
}
|
||||
|
||||
return Card(
|
||||
|
||||
@@ -485,6 +485,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final message = reversedMessages[messageIndex];
|
||||
String fourByteHex = '';
|
||||
if (contact.type == advTypeRoom) {
|
||||
// Room-server messages carry the original author's 4-byte prefix
|
||||
// separately from message.text; use it only for resolving the name.
|
||||
contact = _resolveContactFrom4Bytes(
|
||||
connector,
|
||||
message.fourByteRoomContactKey.isEmpty
|
||||
@@ -509,7 +511,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
? "${contact.name} [$fourByteHex]"
|
||||
: contact.name,
|
||||
sourceId: widget.contact.publicKeyHex,
|
||||
isRoomServer: resolvedContact.type == advTypeRoom,
|
||||
textScale: textScale,
|
||||
onTap: () => _openMessagePath(message, contact),
|
||||
onLongPress: () => _showMessageActions(message, contact),
|
||||
@@ -1577,11 +1578,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
void _showMessageActions(Message message, Contact contact) {
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final translationService = context.read<TranslationService>();
|
||||
final canTranslateMessage =
|
||||
settings.translationEnabled &&
|
||||
!settings.autoTranslateIncomingMessages &&
|
||||
translationService.canTranslateIncoming(
|
||||
text: message.text,
|
||||
isCli: message.isCli,
|
||||
@@ -1748,7 +1746,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
class _MessageBubble extends StatelessWidget {
|
||||
final Message message;
|
||||
final String senderName;
|
||||
final bool isRoomServer;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final void Function(Message message, String emoji)? onRetryReaction;
|
||||
@@ -1759,7 +1756,6 @@ class _MessageBubble extends StatelessWidget {
|
||||
required this.message,
|
||||
required this.senderName,
|
||||
required this.sourceId,
|
||||
required this.isRoomServer,
|
||||
required this.textScale,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
@@ -1785,10 +1781,9 @@ class _MessageBubble extends StatelessWidget {
|
||||
: (isOutgoing ? colorScheme.onPrimary : colorScheme.onSurface);
|
||||
final metaColor = textColor.withValues(alpha: 0.7);
|
||||
const bodyFontSize = 14.0;
|
||||
String messageText = message.text;
|
||||
if (isRoomServer && !isOutgoing) {
|
||||
messageText = message.text.substring(4.clamp(0, message.text.length));
|
||||
}
|
||||
// Do not strip room-server author bytes here: the parser stores them in
|
||||
// fourByteRoomContactKey, so message.text is safe to render as-is.
|
||||
final messageText = message.text;
|
||||
final translatedDisplayText =
|
||||
message.translatedText != null &&
|
||||
message.translatedText!.trim().isNotEmpty
|
||||
|
||||
@@ -430,6 +430,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
selectedIndex: 0,
|
||||
onDestinationSelected: (index) =>
|
||||
_handleQuickSwitch(index, context),
|
||||
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
|
||||
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -539,6 +539,12 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
child: QuickSwitchBar(
|
||||
selectedIndex: 2,
|
||||
onDestinationSelected: (index) => _handleQuickSwitch(index, context),
|
||||
contactsUnreadCount: context
|
||||
.watch<MeshCoreConnector>()
|
||||
.getTotalContactsUnreadCount(),
|
||||
channelsUnreadCount: context
|
||||
.watch<MeshCoreConnector>()
|
||||
.getTotalChannelsUnreadCount(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -676,6 +676,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
selectedIndex: 2,
|
||||
onDestinationSelected: (index) =>
|
||||
_handleQuickSwitch(index, context),
|
||||
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
|
||||
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
|
||||
Reference in New Issue
Block a user