Merge pull request #444 from sethoscope/fix-channel-chat-icon

Use correct channel icons in channel chat screen
This commit is contained in:
zjs81
2026-05-12 09:55:30 -07:00
committed by GitHub
4 changed files with 164 additions and 59 deletions
+34
View File
@@ -4,6 +4,9 @@ import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import '../connector/meshcore_protocol.dart';
import 'community.dart';
enum ChannelType { public, private, hashtag, communityPublic, communityHashtag }
class Channel {
final int index;
@@ -111,5 +114,36 @@ class Channel {
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
static bool isCommunityChannel(ChannelType channelType) {
switch (channelType) {
case ChannelType.communityPublic:
case ChannelType.communityHashtag:
return true;
case ChannelType.public:
case ChannelType.private:
case ChannelType.hashtag:
return false;
}
}
static ChannelType getChannelType(
Channel channel,
CommunityPskIndex communityIndex,
) {
Community? community = communityIndex.getCommunityForChannel(channel);
if (community != null) {
if (Community.isCommunityPublicChannel(channel, community)) {
return ChannelType.communityPublic;
}
return ChannelType.communityHashtag;
}
if (channel.isPublicChannel) {
return ChannelType.public;
} else if (channel.name.startsWith('#')) {
return ChannelType.hashtag;
}
return ChannelType.private;
}
static const String publicChannelPsk = '8b3387e9c5cdea6ac9e5edbaa115cd72';
}
+33
View File
@@ -4,6 +4,8 @@ import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'channel.dart';
/// Represents a community with a shared secret for deriving channel PSKs.
///
/// A Community is a namespace with a shared secret K (32 random bytes),
@@ -162,6 +164,12 @@ class Community {
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
}
/// Returns true if this is the community's public channel
static bool isCommunityPublicChannel(Channel channel, Community community) {
final publicPsk = community.deriveCommunityPublicPsk();
return channel.pskHex == Channel.formatPskHex(publicPsk);
}
/// Add a hashtag channel to this community's list
Community addHashtagChannel(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
@@ -237,3 +245,28 @@ class Community {
@override
int get hashCode => id.hashCode;
}
class CommunityPskIndex {
// Cache of PSK hex -> Community for quick lookup
final Map<String, Community> _pskToCommunity = {};
void initialize(List<Community> communities) {
_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];
}
}
+70 -4
View File
@@ -8,6 +8,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';
@@ -56,8 +58,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;
@@ -81,6 +86,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);
@@ -107,6 +113,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,
@@ -193,16 +212,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(
+27 -55
View File
@@ -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();
@@ -377,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(