mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-08 17:53:25 +10:00
Merge pull request #444 from sethoscope/fix-channel-chat-icon
Use correct channel icons in channel chat screen
This commit is contained in:
@@ -4,6 +4,9 @@ import 'dart:typed_data';
|
|||||||
import 'package:crypto/crypto.dart' as crypto;
|
import 'package:crypto/crypto.dart' as crypto;
|
||||||
|
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import 'community.dart';
|
||||||
|
|
||||||
|
enum ChannelType { public, private, hashtag, communityPublic, communityHashtag }
|
||||||
|
|
||||||
class Channel {
|
class Channel {
|
||||||
final int index;
|
final int index;
|
||||||
@@ -111,5 +114,36 @@ class Channel {
|
|||||||
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
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';
|
static const String publicChannelPsk = '8b3387e9c5cdea6ac9e5edbaa115cd72';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:crypto/crypto.dart' as crypto;
|
import 'package:crypto/crypto.dart' as crypto;
|
||||||
|
|
||||||
|
import 'channel.dart';
|
||||||
|
|
||||||
/// Represents a community with a shared secret for deriving channel PSKs.
|
/// Represents a community with a shared secret for deriving channel PSKs.
|
||||||
///
|
///
|
||||||
/// A Community is a namespace with a shared secret K (32 random bytes),
|
/// 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();
|
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
|
/// Add a hashtag channel to this community's list
|
||||||
Community addHashtagChannel(String hashtag) {
|
Community addHashtagChannel(String hashtag) {
|
||||||
final normalized = _normalizeCommunityHashtag(hashtag);
|
final normalized = _normalizeCommunityHashtag(hashtag);
|
||||||
@@ -237,3 +245,28 @@ class Community {
|
|||||||
@override
|
@override
|
||||||
int get hashCode => id.hashCode;
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
|
import '../models/community.dart';
|
||||||
|
import '../storage/community_store.dart';
|
||||||
import '../utils/platform_info.dart';
|
import '../utils/platform_info.dart';
|
||||||
import '../helpers/chat_scroll_controller.dart';
|
import '../helpers/chat_scroll_controller.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
@@ -56,8 +58,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
final ChatScrollController _scrollController = ChatScrollController();
|
final ChatScrollController _scrollController = ChatScrollController();
|
||||||
final FocusNode _textFieldFocusNode = FocusNode();
|
final FocusNode _textFieldFocusNode = FocusNode();
|
||||||
ChannelMessage? _replyingToMessage;
|
ChannelMessage? _replyingToMessage;
|
||||||
|
final CommunityStore _communityStore = CommunityStore();
|
||||||
|
final CommunityPskIndex _communityIndex = CommunityPskIndex();
|
||||||
final Map<String, GlobalKey> _messageKeys = {};
|
final Map<String, GlobalKey> _messageKeys = {};
|
||||||
bool _isLoadingOlder = false;
|
bool _isLoadingOlder = false;
|
||||||
|
bool _communitiesLoaded = false;
|
||||||
|
|
||||||
MeshCoreConnector? _connector;
|
MeshCoreConnector? _connector;
|
||||||
DateTime? _lastChannelSendAt;
|
DateTime? _lastChannelSendAt;
|
||||||
@@ -81,6 +86,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
final idx = widget.channel.index;
|
final idx = widget.channel.index;
|
||||||
final unread = widget.initialUnreadCount;
|
final unread = widget.initialUnreadCount;
|
||||||
final messages = connector.getChannelMessages(widget.channel);
|
final messages = connector.getChannelMessages(widget.channel);
|
||||||
|
_loadCommunities();
|
||||||
ChannelMessage? anchor;
|
ChannelMessage? anchor;
|
||||||
if (unread > 0) {
|
if (unread > 0) {
|
||||||
anchor = _findOldestUnreadChannelAnchor(messages, unread);
|
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(
|
ChannelMessage? _findOldestUnreadChannelAnchor(
|
||||||
List<ChannelMessage> messages,
|
List<ChannelMessage> messages,
|
||||||
int unreadCount,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
_channelIcon(widget.channel),
|
||||||
widget.channel.isPublicChannel ? Icons.public : Icons.tag,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -44,11 +44,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
with DisconnectNavigationMixin {
|
with DisconnectNavigationMixin {
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final CommunityStore _communityStore = CommunityStore();
|
final CommunityStore _communityStore = CommunityStore();
|
||||||
Timer? _searchDebounce;
|
final CommunityPskIndex _communityIndex = CommunityPskIndex();
|
||||||
List<Community> _communities = [];
|
List<Community> _communities = [];
|
||||||
|
Timer? _searchDebounce;
|
||||||
// Cache of PSK hex -> Community for quick lookup
|
|
||||||
final Map<String, Community> _pskToCommunity = {};
|
|
||||||
|
|
||||||
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
|
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
|
||||||
|
|
||||||
@@ -71,37 +69,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_communities = communities;
|
_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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchDebounce?.cancel();
|
_searchDebounce?.cancel();
|
||||||
@@ -377,37 +349,37 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
int? dragIndex,
|
int? dragIndex,
|
||||||
}) {
|
}) {
|
||||||
final unreadCount = connector.getUnreadCountForChannel(channel);
|
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
|
// Determine icon and colors based on channel type
|
||||||
IconData icon;
|
IconData icon;
|
||||||
Color iconColor;
|
Color iconColor;
|
||||||
Color bgColor;
|
Color bgColor;
|
||||||
|
final ChannelType channelType = Channel.getChannelType(
|
||||||
if (isCommunityChannel) {
|
channel,
|
||||||
// Community channel styling
|
_communityIndex,
|
||||||
iconColor = Colors.purple;
|
);
|
||||||
bgColor = Colors.purple.withValues(alpha: 0.2);
|
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
|
||||||
if (isCommunityPublic) {
|
switch (channelType) {
|
||||||
|
case ChannelType.communityPublic:
|
||||||
icon = Icons.groups;
|
icon = Icons.groups;
|
||||||
} else {
|
iconColor = Colors.purple;
|
||||||
|
bgColor = Colors.purple.withValues(alpha: 0.2);
|
||||||
|
case ChannelType.communityHashtag:
|
||||||
icon = Icons.tag;
|
icon = Icons.tag;
|
||||||
}
|
iconColor = Colors.purple;
|
||||||
} else if (channel.isPublicChannel) {
|
bgColor = Colors.purple.withValues(alpha: 0.2);
|
||||||
icon = Icons.public;
|
case ChannelType.public:
|
||||||
iconColor = Colors.green;
|
icon = Icons.public;
|
||||||
bgColor = Colors.green.withValues(alpha: 0.2);
|
iconColor = Colors.green;
|
||||||
} else if (channel.name.startsWith('#')) {
|
bgColor = Colors.green.withValues(alpha: 0.2);
|
||||||
icon = Icons.tag;
|
case ChannelType.hashtag:
|
||||||
iconColor = Colors.blue;
|
icon = Icons.tag;
|
||||||
bgColor = Colors.blue.withValues(alpha: 0.2);
|
iconColor = Colors.blue;
|
||||||
} else {
|
bgColor = Colors.blue.withValues(alpha: 0.2);
|
||||||
icon = Icons.lock;
|
case ChannelType.private:
|
||||||
iconColor = Colors.blue;
|
icon = Icons.lock;
|
||||||
bgColor = Colors.blue.withValues(alpha: 0.2);
|
iconColor = Colors.blue;
|
||||||
|
bgColor = Colors.blue.withValues(alpha: 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
|
|||||||
Reference in New Issue
Block a user