Files
buzz/mobile/lib/features/channels/channels_page/channel_tile.dart
T
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

242 lines
7.5 KiB
Dart

part of '../channels_page.dart';
class _ChannelTile extends ConsumerWidget {
final Channel channel;
final bool isUnread;
final bool isMuted;
final String? currentPubkey;
final VoidCallback onTap;
/// Called when the user requests to mark this channel read (from long-press
/// actions menu). Null for channels in built-in sections.
final VoidCallback? onMarkRead;
/// The user-defined section this channel currently belongs to, or null.
final String? sectionId;
const _ChannelTile({
required this.channel,
required this.isUnread,
required this.currentPubkey,
required this.onTap,
this.isMuted = false,
this.onMarkRead,
this.sectionId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final contentColor = isMuted
? navigationSecondaryForeground(context)
: navigationPrimaryForeground(
context,
).withValues(alpha: isUnread ? 1 : 0.8);
return InkWell(
borderRadius: BorderRadius.circular(Radii.md),
onTap: onTap,
onLongPress: () => _showChannelActions(context, ref),
child: Padding(
padding: const EdgeInsets.only(
left: _kChannelSectionInset,
right: _kChannelSectionInset,
top: _kChannelRowVerticalPadding,
bottom: _kChannelRowVerticalPadding,
),
child: Row(
children: [
SizedBox(
width: _kChannelLeadingWidth,
child: Align(
alignment: Alignment.centerLeft,
child: channel.isDm
? _DmAvatar(channel: channel, currentPubkey: currentPubkey)
: Icon(
channelIcon(channel),
key: ValueKey('channel-icon-${channel.id}'),
size: _kChannelIconSize,
color: contentColor,
),
),
),
const SizedBox(width: _kChannelLabelGap),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
resolveDmChannelDisplayLabel(
channel,
currentPubkey: currentPubkey,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: contentListTitleTextStyle.copyWith(
color: contentColor,
fontWeight: isUnread ? FontWeight.w700 : FontWeight.w400,
),
),
],
),
),
if (channel.isEphemeral) ...[
const SizedBox(width: Grid.xxs),
_EphemeralBadge(channel: channel),
],
if (isMuted) ...[
const SizedBox(width: Grid.xxs),
Icon(
LucideIcons.bellOff,
size: 12,
color: context.colors.onSurface.withValues(alpha: 0.4),
),
],
if (!channel.isMember && !channel.isDm)
Padding(
padding: const EdgeInsets.only(right: Grid.xxs),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: Grid.half + 2,
vertical: 3,
),
decoration: BoxDecoration(
color: context.colors.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(Radii.sm),
),
child: Text(
context.l10n.openChannel,
style: context.textTheme.labelSmall?.copyWith(
color: context.colors.primary,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
);
}
void _showChannelActions(BuildContext context, WidgetRef ref) {
showChannelActionsSheet(
context: context,
channel: channel,
isUnread: isUnread,
onMarkRead: onMarkRead,
sectionId: sectionId,
);
}
}
class _DmAvatar extends ConsumerWidget {
final Channel channel;
final String? currentPubkey;
const _DmAvatar({required this.channel, required this.currentPubkey});
@override
Widget build(BuildContext context, WidgetRef ref) {
final profiles = ref.watch(userCacheProvider);
final presenceMap = ref.watch(presenceCacheProvider);
final normalizedCurrent = currentPubkey?.toLowerCase();
final otherPubkeys = [
for (final pk in channel.participantPubkeys)
if (pk.toLowerCase() != normalizedCurrent) pk.toLowerCase(),
];
final visiblePubkeys = otherPubkeys.isNotEmpty
? otherPubkeys
: channel.participantPubkeys.map((pk) => pk.toLowerCase()).toList();
if (visiblePubkeys.length > 1) {
return Container(
width: _kDmAvatarSize,
height: _kDmAvatarSize,
alignment: Alignment.center,
decoration: BoxDecoration(
color: context.colors.surfaceContainerHighest,
shape: BoxShape.circle,
border: Border.all(color: context.colors.outlineVariant),
),
child: Text(
'${visiblePubkeys.length}',
style: context.textTheme.labelSmall?.copyWith(
fontSize: 9,
color: context.colors.onSurface,
fontWeight: FontWeight.w600,
height: 1,
),
),
);
}
final otherPubkey = visiblePubkeys.isNotEmpty ? visiblePubkeys.first : null;
final profile = otherPubkey != null ? profiles[otherPubkey] : null;
// Trigger fetches if not cached yet.
if (otherPubkey != null) {
if (profile == null) {
ref.read(userCacheProvider.notifier).preload([otherPubkey]);
}
ref.read(presenceCacheProvider.notifier).track([otherPubkey]);
}
final avatarUrl = profile?.avatarUrl;
final initial =
profile?.initial ??
(channel.participants.isNotEmpty
? channel.participants.first[0].toUpperCase()
: '?');
final presence = otherPubkey != null
? (presenceMap[otherPubkey] ?? 'offline')
: 'offline';
return SizedBox(
width: _kDmAvatarSize,
height: _kDmAvatarSize,
child: Stack(
clipBehavior: Clip.none,
children: [
AvatarImage(
imageUrl: avatarUrl,
radius: _kDmAvatarSize / 2,
backgroundColor: context.colors.primaryContainer,
fallback: Text(
initial,
style: context.textTheme.labelSmall?.copyWith(
fontSize: 9,
color: context.colors.onPrimaryContainer,
fontWeight: FontWeight.w600,
),
),
),
Positioned(
right: -1,
bottom: -1,
child: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: _presenceColor(context, presence),
shape: BoxShape.circle,
border: Border.all(
color: context.theme.scaffoldBackgroundColor,
width: 1.5,
),
),
),
),
],
),
);
}
Color _presenceColor(BuildContext context, String presence) {
return switch (presence) {
'online' => context.appColors.success,
'away' => context.appColors.warning,
_ => context.colors.outline,
};
}
}