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,246 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../relay/relay.dart';
|
||||
import '../theme/theme_provider.dart';
|
||||
import '../community/community_provider.dart';
|
||||
import 'read_state_manager.dart';
|
||||
|
||||
class ReadStateState {
|
||||
final bool isReady;
|
||||
final String? pubkey;
|
||||
final Map<String, int> contexts;
|
||||
final int version;
|
||||
|
||||
/// Session-local forced-unread flags, keyed by the forced context id
|
||||
/// (a channel id from the channel tile, or a `msg:` key from the message
|
||||
/// actions sheet), each mapped to the channel it belongs to so channel
|
||||
/// tiles and badges can surface message-level forces.
|
||||
final Map<String, String> forcedUnreadContexts;
|
||||
|
||||
const ReadStateState({
|
||||
required this.isReady,
|
||||
required this.pubkey,
|
||||
required this.contexts,
|
||||
required this.version,
|
||||
this.forcedUnreadContexts = const {},
|
||||
});
|
||||
|
||||
const ReadStateState.inert()
|
||||
: isReady = false,
|
||||
pubkey = null,
|
||||
contexts = const {},
|
||||
version = 0,
|
||||
forcedUnreadContexts = const {};
|
||||
|
||||
/// Channels that should surface as unread because of a forced-unread flag —
|
||||
/// either forced directly, or containing a forced message.
|
||||
Set<String> get locallyForcedChannelIds =>
|
||||
forcedUnreadContexts.values.toSet();
|
||||
|
||||
/// Whether this exact context (channel id or `msg:` key) is forced unread.
|
||||
bool isForcedUnread(String contextId) =>
|
||||
forcedUnreadContexts.containsKey(contextId);
|
||||
|
||||
int? effectiveTimestamp(String contextId) => contexts[contextId];
|
||||
|
||||
ReadStateState copyWithContext(String contextId, int timestamp) {
|
||||
final current = contexts[contextId] ?? 0;
|
||||
if (timestamp <= current) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return ReadStateState(
|
||||
isReady: isReady,
|
||||
pubkey: pubkey,
|
||||
contexts: Map.unmodifiable({...contexts, contextId: timestamp}),
|
||||
version: version + 1,
|
||||
forcedUnreadContexts: forcedUnreadContexts,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReadStateNotifier extends Notifier<ReadStateState> {
|
||||
ReadStateManager? _manager;
|
||||
bool _isInitialized = false;
|
||||
final Map<String, String> _forcedUnreadContexts = {};
|
||||
|
||||
@override
|
||||
ReadStateState build() {
|
||||
_manager?.dispose(flushPending: false);
|
||||
_manager = null;
|
||||
_isInitialized = false;
|
||||
_forcedUnreadContexts.clear();
|
||||
|
||||
final relayConfig = ref.watch(relayConfigProvider);
|
||||
ref.watch(relaySessionProvider);
|
||||
final activeCommunity = ref.watch(activeCommunityProvider).value;
|
||||
|
||||
final nsec = relayConfig.nsec?.trim();
|
||||
if (nsec == null || nsec.isEmpty) {
|
||||
return const ReadStateState.inert();
|
||||
}
|
||||
|
||||
final signedRelay = SignedEventRelay(
|
||||
session: ref.read(relaySessionProvider.notifier),
|
||||
nsec: nsec,
|
||||
);
|
||||
final pubkey =
|
||||
_normalizePubkey(activeCommunity?.pubkey) ??
|
||||
_safeDerivedPubkey(signedRelay);
|
||||
if (pubkey == null) {
|
||||
return const ReadStateState.inert();
|
||||
}
|
||||
|
||||
final crypto = ReadStateCrypto.tryCreate(nsec: nsec, pubkey: pubkey);
|
||||
if (crypto == null) {
|
||||
return const ReadStateState.inert();
|
||||
}
|
||||
|
||||
final prefs = ref.read(savedPrefsProvider);
|
||||
late final ReadStateManager manager;
|
||||
manager = ReadStateManager(
|
||||
pubkey: pubkey,
|
||||
prefs: prefs,
|
||||
crypto: crypto,
|
||||
relaySession: ref.read(relaySessionProvider.notifier),
|
||||
signedEventRelay: signedRelay,
|
||||
remoteEnabled: true,
|
||||
onChanged: () => _emitManagerState(manager),
|
||||
);
|
||||
_manager = manager;
|
||||
|
||||
ref.onDispose(() {
|
||||
manager.dispose();
|
||||
if (_manager == manager) {
|
||||
_manager = null;
|
||||
}
|
||||
});
|
||||
|
||||
ref.listen(appLifecycleProvider, (_, next) {
|
||||
if (next == AppLifecycleState.paused ||
|
||||
next == AppLifecycleState.detached ||
|
||||
next == AppLifecycleState.hidden) {
|
||||
unawaited(manager.flush());
|
||||
}
|
||||
});
|
||||
|
||||
ref.listen(relaySessionProvider, (prev, next) {
|
||||
if (prev?.status != SessionStatus.connected &&
|
||||
next.status == SessionStatus.connected) {
|
||||
unawaited(manager.reinitializeRemote());
|
||||
}
|
||||
});
|
||||
|
||||
Future.microtask(() async {
|
||||
await manager.initialize();
|
||||
if (_manager != manager) return;
|
||||
_isInitialized = true;
|
||||
_emitManagerState(manager);
|
||||
});
|
||||
|
||||
return _stateFromManager(manager, isReady: false);
|
||||
}
|
||||
|
||||
/// Advance a context's read marker. Clears the forced-unread flag for
|
||||
/// exactly this context, if any. An explicit channel-level "Mark read"
|
||||
/// (channel tile/menu) should pass [clearForcedMessages] so message-level
|
||||
/// forces inside the channel are released too; the automatic read on
|
||||
/// channel open must not, so a message deliberately marked unread stays
|
||||
/// unread until acted on.
|
||||
void markContextRead(
|
||||
String contextId,
|
||||
int unixTimestamp, {
|
||||
bool clearForcedMessages = false,
|
||||
}) {
|
||||
var removed = _forcedUnreadContexts.remove(contextId) != null;
|
||||
if (clearForcedMessages) {
|
||||
final before = _forcedUnreadContexts.length;
|
||||
_forcedUnreadContexts.removeWhere(
|
||||
(_, channelId) => channelId == contextId,
|
||||
);
|
||||
removed = removed || _forcedUnreadContexts.length != before;
|
||||
}
|
||||
_manager?.markContextRead(contextId, unixTimestamp);
|
||||
if (removed) {
|
||||
_refreshForcedState();
|
||||
}
|
||||
}
|
||||
|
||||
/// Force a context unread for the rest of the session. [contextId] is a
|
||||
/// channel id (channel-tile action) or a `msg:` key (message actions
|
||||
/// sheet); [channelId] is the channel the context belongs to, so tiles and
|
||||
/// badges can surface message-level forces. Read markers are monotonic,
|
||||
/// so this is the only way to move a context back to unread.
|
||||
void markContextUnread(String contextId, {required String channelId}) {
|
||||
if (_manager == null) return;
|
||||
_forcedUnreadContexts[contextId] = channelId;
|
||||
_refreshForcedState();
|
||||
}
|
||||
|
||||
void _refreshForcedState() {
|
||||
final manager = _manager;
|
||||
if (manager == null) return;
|
||||
state = _stateFromManager(
|
||||
manager,
|
||||
isReady: _isInitialized,
|
||||
previousVersion: state.version,
|
||||
);
|
||||
}
|
||||
|
||||
void seedContextRead(String contextId, int unixTimestamp) {
|
||||
_manager?.seedContextRead(contextId, unixTimestamp);
|
||||
}
|
||||
|
||||
void _emitManagerState(ReadStateManager manager) {
|
||||
if (_manager != manager) return;
|
||||
final advances = manager.drainSyncedAdvances();
|
||||
for (final contextId in advances) {
|
||||
_forcedUnreadContexts.remove(contextId);
|
||||
}
|
||||
state = _stateFromManager(
|
||||
manager,
|
||||
isReady: _isInitialized,
|
||||
previousVersion: state.version,
|
||||
);
|
||||
}
|
||||
|
||||
ReadStateState _stateFromManager(
|
||||
ReadStateManager manager, {
|
||||
required bool isReady,
|
||||
int? previousVersion,
|
||||
}) {
|
||||
return ReadStateState(
|
||||
isReady: isReady,
|
||||
pubkey: manager.pubkey,
|
||||
contexts: manager.effectiveContexts,
|
||||
version: (previousVersion ?? 0) + 1,
|
||||
forcedUnreadContexts: Map.unmodifiable(
|
||||
Map<String, String>.from(_forcedUnreadContexts),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final readStateProvider = NotifierProvider<ReadStateNotifier, ReadStateState>(
|
||||
ReadStateNotifier.new,
|
||||
);
|
||||
|
||||
String? _normalizePubkey(String? value) {
|
||||
final normalized = value?.trim().toLowerCase();
|
||||
if (normalized == null || normalized.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
String? _safeDerivedPubkey(SignedEventRelay relay) {
|
||||
try {
|
||||
return _normalizePubkey(relay.pubkey);
|
||||
} catch (e) {
|
||||
debugPrint('[ReadStateManager] pubkey derivation failed: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user