feat: import Chinese-localized Buzz source snapshot
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
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
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
part of '../channels_page.dart';
|
||||
|
||||
class _ChannelsBody extends StatelessWidget {
|
||||
final List<Channel>? channels;
|
||||
final AsyncValue<List<Channel>> channelsAsync;
|
||||
final bool showError;
|
||||
final SessionStatus sessionStatus;
|
||||
final bool showConnectionSkeleton;
|
||||
final String? currentPubkey;
|
||||
final double topSectionHeight;
|
||||
final bool usesPinnedGradient;
|
||||
final ScrollController scrollController;
|
||||
final Future<void> Function() onRefresh;
|
||||
final Future<void> Function(Channel channel) onSelectChannel;
|
||||
|
||||
const _ChannelsBody({
|
||||
required this.channels,
|
||||
required this.channelsAsync,
|
||||
required this.showError,
|
||||
required this.sessionStatus,
|
||||
required this.showConnectionSkeleton,
|
||||
required this.currentPubkey,
|
||||
required this.topSectionHeight,
|
||||
required this.usesPinnedGradient,
|
||||
required this.scrollController,
|
||||
required this.onRefresh,
|
||||
required this.onSelectChannel,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final barHeight = topSectionHeight;
|
||||
final loadedChannels = channels;
|
||||
final loading =
|
||||
showConnectionSkeleton || (loadedChannels == null && !showError);
|
||||
final content = showError && channelsAsync.hasError
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(top: barHeight),
|
||||
child: _ErrorView(error: channelsAsync.error!, onRetry: onRefresh),
|
||||
)
|
||||
: loadedChannels == null
|
||||
? const SizedBox.shrink()
|
||||
: BeeRefreshIndicator(
|
||||
edgeOffset: barHeight,
|
||||
onRefresh: onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
// The transparent gap shows the top section and must not absorb
|
||||
// taps meant for the community or profile controls beneath it.
|
||||
hitTestBehavior: HitTestBehavior.deferToChild,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: SizedBox(height: barHeight)),
|
||||
if (usesPinnedGradient)
|
||||
_SliverChannelsList(
|
||||
channels: loadedChannels,
|
||||
currentPubkey: currentPubkey,
|
||||
onSelectChannel: onSelectChannel,
|
||||
)
|
||||
else
|
||||
DecoratedSliver(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.surface,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(Radii.dialog),
|
||||
),
|
||||
),
|
||||
sliver: _SliverChannelsList(
|
||||
channels: loadedChannels,
|
||||
currentPubkey: currentPubkey,
|
||||
onSelectChannel: onSelectChannel,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return SkeletonReveal(
|
||||
loading: loading,
|
||||
shimmerEnabled: sessionStatus != SessionStatus.disconnected,
|
||||
skeleton: _ChannelsSkeleton(
|
||||
channels: loadedChannels,
|
||||
topInset: barHeight,
|
||||
status: sessionStatus,
|
||||
),
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SliverChannelsList extends HookConsumerWidget {
|
||||
final List<Channel> channels;
|
||||
final String? currentPubkey;
|
||||
final Future<void> Function(Channel channel) onSelectChannel;
|
||||
|
||||
const _SliverChannelsList({
|
||||
required this.channels,
|
||||
required this.currentPubkey,
|
||||
required this.onSelectChannel,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final readState = ref.watch(readStateProvider);
|
||||
final sectionsState = ref.watch(channelSectionsProvider);
|
||||
final mutesState = ref.watch(channelMutesProvider);
|
||||
final mutedChannelIds = {
|
||||
for (final entry in mutesState.store.channels.entries)
|
||||
if (entry.value.muted) entry.key,
|
||||
};
|
||||
final starsState = ref.watch(channelStarsProvider);
|
||||
final starredChannelIds = {
|
||||
for (final entry in starsState.store.channels.entries)
|
||||
if (entry.value.starred) entry.key,
|
||||
};
|
||||
final visibleChannels = channels
|
||||
.where((channel) => channel.isMember && !channel.isArchived)
|
||||
.toList();
|
||||
final streamChannels = visibleChannels
|
||||
.where((channel) => channel.isStream)
|
||||
.toList();
|
||||
final dmChannels = sortDmChannelsByDisplayLabel(
|
||||
visibleChannels.where((channel) => channel.isDm),
|
||||
currentPubkey: currentPubkey,
|
||||
);
|
||||
|
||||
final starredExpanded = useState(true);
|
||||
final channelsExpanded = useState(true);
|
||||
final dmsExpanded = useState(true);
|
||||
final sortState = ref.watch(channelSortProvider);
|
||||
final initialSeedComplete = useState(false);
|
||||
final seededPubkey = useRef<String?>(null);
|
||||
final seedCompleteForPubkey =
|
||||
seededPubkey.value == readState.pubkey && initialSeedComplete.value;
|
||||
|
||||
useEffect(() {
|
||||
if (!readState.isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return deferReadStateUpdate(context, () {
|
||||
if (seededPubkey.value != readState.pubkey) {
|
||||
seededPubkey.value = readState.pubkey;
|
||||
initialSeedComplete.value = false;
|
||||
}
|
||||
|
||||
if (initialSeedComplete.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
final notifier = ref.read(readStateProvider.notifier);
|
||||
for (final channel in visibleChannels) {
|
||||
if (readState.effectiveTimestamp(channel.id) != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final lastMessageAt = dateTimeToUnixSeconds(channel.lastMessageAt);
|
||||
if (lastMessageAt != null) {
|
||||
notifier.seedContextRead(channel.id, lastMessageAt);
|
||||
}
|
||||
}
|
||||
initialSeedComplete.value = true;
|
||||
});
|
||||
}, [readState.isReady, readState.pubkey, visibleChannels]);
|
||||
|
||||
final unreadState = _computeUnreadChannelState(
|
||||
channels: visibleChannels,
|
||||
readState: readState,
|
||||
channelsNotifier: ref.read(channelsProvider.notifier),
|
||||
);
|
||||
final unreadChannelIds = {
|
||||
for (final channelId in unreadState.ids)
|
||||
if (seedCompleteForPubkey ||
|
||||
readState.effectiveTimestamp(channelId) != null)
|
||||
channelId,
|
||||
};
|
||||
// Build sorted user-defined sections and compute which stream channels
|
||||
// belong to each section. Channels not assigned to any valid section fall
|
||||
// through to the built-in "Channels" list.
|
||||
final userSections = sectionsState.store.sections.toList()
|
||||
..sort((a, b) => a.order.compareTo(b.order));
|
||||
final sectionAssignments = sectionsState.store.assignments;
|
||||
final validSectionIds = {for (final s in userSections) s.id};
|
||||
final assignedChannelIds = {
|
||||
for (final entry in sectionAssignments.entries)
|
||||
if (validSectionIds.contains(entry.value)) entry.key,
|
||||
};
|
||||
// Starred is exclusive: a starred channel lives only in the Starred section,
|
||||
// not in its custom section or the default Channels list.
|
||||
final starredStreamChannels = sortChannelsForList(
|
||||
streamChannels.where((c) => starredChannelIds.contains(c.id)).toList(),
|
||||
sortState.sortModeFor('starred'),
|
||||
);
|
||||
final ungroupedStreamChannels = sortChannelsForList(
|
||||
streamChannels
|
||||
.where(
|
||||
(c) =>
|
||||
!assignedChannelIds.contains(c.id) &&
|
||||
!starredChannelIds.contains(c.id),
|
||||
)
|
||||
.toList(),
|
||||
sortState.sortModeFor('channels'),
|
||||
);
|
||||
// DMs default to the display-label alphabetical order (labels can differ
|
||||
// from channel names); Recent mode reorders by last message time.
|
||||
final sortedDmChannels =
|
||||
sortState.sortModeFor('dms') == ChannelSortMode.recent
|
||||
? sortChannelsForList(dmChannels, ChannelSortMode.recent)
|
||||
: dmChannels;
|
||||
|
||||
final liveSectionIds = [for (final s in userSections) s.id];
|
||||
void setSortMode(String groupKey, ChannelSortMode mode) {
|
||||
ref
|
||||
.read(channelSortProvider.notifier)
|
||||
.setSortModeFor(groupKey, mode, liveSectionIds: liveSectionIds);
|
||||
}
|
||||
|
||||
final sectionExpandedStates = useState<Map<String, bool>>({});
|
||||
|
||||
bool sectionExpanded(String sectionId) =>
|
||||
sectionExpandedStates.value[sectionId] ?? true;
|
||||
|
||||
void toggleSection(String sectionId) {
|
||||
sectionExpandedStates.value = {
|
||||
...sectionExpandedStates.value,
|
||||
sectionId: !sectionExpanded(sectionId),
|
||||
};
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: Grid.xxs,
|
||||
bottom: MediaQuery.paddingOf(context).bottom,
|
||||
),
|
||||
sliver: SliverList.list(
|
||||
children: [
|
||||
if (visibleChannels.isEmpty)
|
||||
const _EmptyState()
|
||||
else ...[
|
||||
// Starred channels (exclusive — pinned above all sections).
|
||||
if (starredStreamChannels.isNotEmpty)
|
||||
_ChannelSection(
|
||||
title: context.l10n.starred,
|
||||
icon: LucideIcons.star,
|
||||
showTopDivider: false,
|
||||
expanded: starredExpanded.value,
|
||||
onToggle: () => starredExpanded.value = !starredExpanded.value,
|
||||
channels: starredStreamChannels,
|
||||
unreadChannelIds: unreadChannelIds,
|
||||
mutedChannelIds: mutedChannelIds,
|
||||
currentPubkey: currentPubkey,
|
||||
emptyLabel: '',
|
||||
sortMode: sortState.sortModeFor('starred'),
|
||||
onSortModeChange: (mode) => setSortMode('starred', mode),
|
||||
onSelectChannel: onSelectChannel,
|
||||
),
|
||||
// User-defined sections for stream channels, in user-defined order.
|
||||
for (final section in userSections)
|
||||
_CustomChannelSection(
|
||||
section: section,
|
||||
channels: sortChannelsForList(
|
||||
streamChannels
|
||||
.where(
|
||||
(c) =>
|
||||
sectionAssignments[c.id] == section.id &&
|
||||
!starredChannelIds.contains(c.id),
|
||||
)
|
||||
.toList(),
|
||||
sortState.sortModeFor(sectionSortGroupKey(section.id)),
|
||||
),
|
||||
unreadChannelIds: unreadChannelIds,
|
||||
mutedChannelIds: mutedChannelIds,
|
||||
currentPubkey: currentPubkey,
|
||||
expanded: sectionExpanded(section.id),
|
||||
isFirst: userSections.first.id == section.id,
|
||||
isLast: userSections.last.id == section.id,
|
||||
showTopDivider:
|
||||
starredStreamChannels.isNotEmpty ||
|
||||
userSections.first.id != section.id,
|
||||
onToggle: () => toggleSection(section.id),
|
||||
onRename: () async {
|
||||
final name = await showBuzzDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => _SectionNameDialog(
|
||||
title: context.l10n.renameSection,
|
||||
confirmLabel: context.l10n.rename,
|
||||
initialValue: section.name,
|
||||
),
|
||||
);
|
||||
if (name != null && name.isNotEmpty) {
|
||||
ref
|
||||
.read(channelSectionsProvider.notifier)
|
||||
.renameSection(section.id, name);
|
||||
}
|
||||
},
|
||||
onDelete: () async {
|
||||
final confirmed = await showBuzzDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text(context.l10n.deleteSectionNamed(section.name)),
|
||||
content: Text(context.l10n.deleteSectionConfirm),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(context.l10n.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(
|
||||
context.l10n.delete,
|
||||
style: TextStyle(color: context.colors.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
ref
|
||||
.read(channelSectionsProvider.notifier)
|
||||
.deleteSection(section.id);
|
||||
}
|
||||
},
|
||||
onMoveUp: () => ref
|
||||
.read(channelSectionsProvider.notifier)
|
||||
.moveSectionUp(section.id),
|
||||
onMoveDown: () => ref
|
||||
.read(channelSectionsProvider.notifier)
|
||||
.moveSectionDown(section.id),
|
||||
sortMode: sortState.sortModeFor(
|
||||
sectionSortGroupKey(section.id),
|
||||
),
|
||||
onSortModeChange: (mode) =>
|
||||
setSortMode(sectionSortGroupKey(section.id), mode),
|
||||
onSelectChannel: onSelectChannel,
|
||||
onMarkChannelRead: (channel) {
|
||||
final ts = dateTimeToUnixSeconds(channel.lastMessageAt);
|
||||
if (ts != null) {
|
||||
ref
|
||||
.read(readStateProvider.notifier)
|
||||
.markContextRead(
|
||||
channel.id,
|
||||
ts,
|
||||
clearForcedMessages: true,
|
||||
);
|
||||
ref
|
||||
.read(channelsProvider.notifier)
|
||||
.clearObservedUnreadCoveredByRead(channel.id, ts);
|
||||
}
|
||||
},
|
||||
),
|
||||
_ChannelSection(
|
||||
title: context.l10n.searchChannels,
|
||||
icon: LucideIcons.hash,
|
||||
showTopDivider:
|
||||
starredStreamChannels.isNotEmpty || userSections.isNotEmpty,
|
||||
expanded: channelsExpanded.value,
|
||||
onToggle: () => channelsExpanded.value = !channelsExpanded.value,
|
||||
channels: ungroupedStreamChannels,
|
||||
unreadChannelIds: unreadChannelIds,
|
||||
mutedChannelIds: mutedChannelIds,
|
||||
currentPubkey: currentPubkey,
|
||||
emptyLabel: context.l10n.noStreamChannels,
|
||||
sortMode: sortState.sortModeFor('channels'),
|
||||
onSortModeChange: (mode) => setSortMode('channels', mode),
|
||||
onSelectChannel: onSelectChannel,
|
||||
),
|
||||
_ChannelSection(
|
||||
title: context.l10n.directMessages,
|
||||
icon: LucideIcons.messagesSquare,
|
||||
showTopDivider: true,
|
||||
expanded: dmsExpanded.value,
|
||||
onToggle: () => dmsExpanded.value = !dmsExpanded.value,
|
||||
channels: sortedDmChannels,
|
||||
unreadChannelIds: unreadChannelIds,
|
||||
mutedChannelIds: mutedChannelIds,
|
||||
currentPubkey: currentPubkey,
|
||||
emptyLabel: context.l10n.noDirectMessages,
|
||||
sortMode: sortState.sortModeFor('dms'),
|
||||
onSortModeChange: (mode) => setSortMode('dms', mode),
|
||||
onSelectChannel: onSelectChannel,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user