diff --git a/analysis_options.yaml b/analysis_options.yaml index d34948dbb..627d9218b 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,7 +7,10 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +include: + - package:flutter_lints/flutter.yaml + - package:bloc_lint/recommended.yaml + analyzer: errors: use_build_context_synchronously: ignore diff --git a/lib/packages/ui/src/widgets/media/thunder_image_viewer.dart b/lib/packages/ui/src/widgets/media/thunder_image_viewer.dart new file mode 100644 index 000000000..504de106a --- /dev/null +++ b/lib/packages/ui/src/widgets/media/thunder_image_viewer.dart @@ -0,0 +1,682 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_avif/flutter_avif.dart'; + +/// Describes the image content displayed by a [ThunderImageViewer]. +/// +/// Create a source with either [ThunderImageViewerSource.network] for cached +/// network images or [ThunderImageViewerSource.memory] for in-memory bytes. +/// +/// The optional [contentType] can be used to hint the image format when the URL +/// alone is not sufficient, such as for AVIF images served without an `.avif` +/// file extension. +sealed class ThunderImageViewerSource { + /// Creates a descriptor for image content shown by a + /// [ThunderImageViewer]. + const ThunderImageViewerSource({this.contentType}); + + /// Creates a source backed by raw in-memory image bytes. + const factory ThunderImageViewerSource.memory( + Uint8List bytes, { + String? contentType, + }) = ThunderImageViewerMemorySource; + + /// Creates a source backed by a network image URL. + const factory ThunderImageViewerSource.network( + String url, { + String? contentType, + }) = ThunderImageViewerNetworkSource; + + /// The MIME content type associated with this image, if known. + final String? contentType; +} + +/// A [ThunderImageViewerSource] backed by image bytes held in memory. +class ThunderImageViewerMemorySource extends ThunderImageViewerSource { + /// Creates a memory-backed image source. + const ThunderImageViewerMemorySource(this.bytes, {super.contentType}); + + /// The encoded image bytes to display. + final Uint8List bytes; +} + +/// A [ThunderImageViewerSource] backed by a network URL. +class ThunderImageViewerNetworkSource extends ThunderImageViewerSource { + /// Creates a network-backed image source. + const ThunderImageViewerNetworkSource(this.url, {super.contentType}); + + /// The URL used to fetch and cache the image. + final String url; +} + +enum _ViewerGestureMode { + idle, + transform, + dismiss, +} + +class ThunderImageViewer extends StatefulWidget { + /// Creates an interactive image viewer. + /// + /// [source] must not be null. + /// + /// The viewer supports: + /// + /// * single-tap callbacks through [onTap] + /// * double-tap zoom using [doubleTapScales] + /// * double-tap-and-drag zoom + /// * pinch zoom and panning + /// * optional drag-to-dismiss through [dismissible] and [onDismiss] + const ThunderImageViewer({ + super.key, + required this.source, + this.backgroundColor = Colors.black, + this.contentSize, + this.dismissible = true, + this.doubleTapScales = const [2.0, 4.0], + this.errorBuilder, + this.filterQuality = FilterQuality.medium, + this.loadingBuilder, + this.maxScale = 4.0, + this.minScale = 1.0, + this.onDismiss, + this.onLongPress, + this.onScaleChanged, + this.onTap, + this.semanticLabel, + }) : assert(minScale > 0), + assert(maxScale >= minScale); + + /// The color painted behind the image. + /// + /// This color also fades during a drag-to-dismiss gesture. + final Color backgroundColor; + + /// The intrinsic size of the image content, if known. + /// + /// When provided, the viewer uses this to compute a more accurate contained + /// layout before the image is transformed. + final Size? contentSize; + + /// Whether a vertical drag can dismiss the viewer when it is at rest. + /// + /// Dismiss gestures are ignored while zoomed in or while another transform + /// gesture is active. + final bool dismissible; + + /// The zoom levels cycled through by a double tap. + /// + /// Once all values in this list have been used, the next double tap resets the + /// viewer back to [minScale]. + final List doubleTapScales; + + /// Builds a widget shown when the image fails to load. + /// + /// If null, a default broken-image icon is shown. + final Widget Function(BuildContext context, Object error)? errorBuilder; + + /// The filter quality used when painting the image. + final FilterQuality filterQuality; + + /// Builds a widget shown while a network image is still loading. + /// + /// If null, a [CircularProgressIndicator] is displayed. + final WidgetBuilder? loadingBuilder; + + /// The maximum zoom scale allowed by user gestures. + final double maxScale; + + /// The minimum zoom scale allowed by user gestures. + /// + /// This is typically `1.0`, which represents the image's contained layout. + final double minScale; + + /// Called when the viewer has been dragged far enough to dismiss. + /// + /// This callback is only invoked when [dismissible] is true. + final VoidCallback? onDismiss; + + /// Called when the viewer detects a long press. + final VoidCallback? onLongPress; + + /// Called whenever the effective zoom scale changes. + /// + /// This includes animated double-tap zoom, double-tap-and-drag zoom, pinch + /// gestures, and snap-back corrections after a gesture ends. + final ValueChanged? onScaleChanged; + + /// Called after a completed single tap. + /// + /// Single taps are delayed slightly so the viewer can distinguish them from a + /// double tap. + final VoidCallback? onTap; + + /// The semantic label used for the underlying image. + final String? semanticLabel; + + /// The source describing the image to display. + final ThunderImageViewerSource source; + + @override + State createState() => _ThunderImageViewerState(); +} + +class _ThunderImageViewerState extends State with TickerProviderStateMixin { + static const Duration _doubleTapTimeout = Duration(milliseconds: 280); + static const double _doubleTapDragSlop = 8; + static const double _doubleTapSlop = 36; + static const double _gestureEpsilon = 0.01; + + late final AnimationController _transformAnimationController; + + Animation? _scaleAnimation; + Animation? _offsetAnimation; + Animation? _dismissOffsetAnimation; + + Size _baseContentSize = Size.zero; + Size _viewportSize = Size.zero; + + double _gestureStartScale = 1.0; + double _scale = 1.0; + double _doubleTapBaseScale = 1.0; + + Timer? _singleTapTimer; + DateTime? _lastTapUpTime; + + Offset _dismissOffset = Offset.zero; + Offset _gestureStartOffset = Offset.zero; + Offset _offset = Offset.zero; + Offset _doubleTapBaseOffset = Offset.zero; + Offset? _doubleTapAnchor; + Offset? _doubleTapStartLocalPosition; + Offset? _gestureStartFocalPoint; + Offset? _lastTapUpPosition; + Offset? _primaryDownPosition; + + _ViewerGestureMode _gestureMode = _ViewerGestureMode.idle; + + int _activePointers = 0; + int? _doubleTapPointer; + int? _primaryTapPointer; + + bool _dragZoomActive = false; + bool _didTriggerLongPress = false; + bool _ignoreScaleEnd = false; + bool _tapMoved = false; + + void _setScale(double value) { + if ((_scale - value).abs() <= _gestureEpsilon) { + _scale = value; + return; + } + + _scale = value; + widget.onScaleChanged?.call(value); + } + + void _handleLongPress() { + _didTriggerLongPress = true; + widget.onLongPress?.call(); + } + + @override + void initState() { + super.initState(); + _transformAnimationController = AnimationController(vsync: this)..addListener(_handleTransformAnimationTick); + } + + @override + void dispose() { + _singleTapTimer?.cancel(); + _transformAnimationController.dispose(); + super.dispose(); + } + + void _handleTransformAnimationTick() { + if (!mounted) return; + + setState(() { + if (_scaleAnimation != null) { + _setScale(_scaleAnimation!.value); + } + if (_offsetAnimation != null) { + _offset = _offsetAnimation!.value; + } + if (_dismissOffsetAnimation != null) { + _dismissOffset = _dismissOffsetAnimation!.value; + } + }); + } + + void _animateTo({ + Duration duration = const Duration(milliseconds: 180), + Curve curve = Curves.easeOutCubic, + required double targetScale, + required Offset targetOffset, + Offset targetDismissOffset = Offset.zero, + }) { + _transformAnimationController + ..stop() + ..duration = duration; + + final animation = CurvedAnimation(parent: _transformAnimationController, curve: curve); + + _scaleAnimation = Tween(begin: _scale, end: targetScale).animate(animation); + _offsetAnimation = Tween(begin: _offset, end: targetOffset).animate(animation); + _dismissOffsetAnimation = Tween(begin: _dismissOffset, end: targetDismissOffset).animate(animation); + + _transformAnimationController + ..reset() + ..forward(); + } + + Size _resolveBaseContentSize(Size viewportSize) { + final contentSize = widget.contentSize; + + if (contentSize == null || contentSize.width <= 0 || contentSize.height <= 0) { + return viewportSize; + } + + return applyBoxFit(BoxFit.contain, contentSize, viewportSize).destination; + } + + Offset _maxPanOffset(Size baseContentSize, double scale) { + final scaledWidth = baseContentSize.width * scale; + final scaledHeight = baseContentSize.height * scale; + + return Offset( + math.max((scaledWidth - _viewportSize.width) / 2, 0), + math.max((scaledHeight - _viewportSize.height) / 2, 0), + ); + } + + Offset _clampOffset(Offset offset, Size baseContentSize, double scale) { + if (_viewportSize == Size.zero || scale <= widget.minScale + _gestureEpsilon) { + return Offset.zero; + } + + final maxOffset = _maxPanOffset(baseContentSize, scale); + + return Offset( + offset.dx.clamp(-maxOffset.dx, maxOffset.dx), + offset.dy.clamp(-maxOffset.dy, maxOffset.dy), + ); + } + + Offset _anchoredOffsetForScale({ + required Offset anchor, + required Offset baseOffset, + required double baseScale, + required double targetScale, + }) { + final viewportCenter = _viewportSize.center(Offset.zero); + final contentPoint = (anchor - viewportCenter - baseOffset) / baseScale; + final targetOffset = anchor - viewportCenter - contentPoint * targetScale; + + return _clampOffset(targetOffset, _baseContentSize, targetScale); + } + + void _snapBackIntoBounds() { + final targetScale = _scale <= widget.minScale + _gestureEpsilon ? widget.minScale : _scale; + final targetOffset = targetScale <= widget.minScale + _gestureEpsilon ? Offset.zero : _clampOffset(_offset, _baseContentSize, targetScale); + + if ((targetScale - _scale).abs() <= _gestureEpsilon && (targetOffset - _offset).distance <= _gestureEpsilon && _dismissOffset.distance <= _gestureEpsilon) { + return; + } + + _animateTo(targetScale: targetScale, targetOffset: targetOffset); + } + + double _nextDoubleTapScale() { + for (final scale in widget.doubleTapScales) { + if (_scale < scale - _gestureEpsilon) { + return scale.clamp(widget.minScale, widget.maxScale); + } + } + + return widget.minScale; + } + + void _handleDoubleTap(Offset anchor) { + final targetScale = _nextDoubleTapScale(); + final targetOffset = targetScale <= widget.minScale + _gestureEpsilon + ? Offset.zero + : _anchoredOffsetForScale( + anchor: anchor, + baseOffset: _offset, + baseScale: _scale, + targetScale: targetScale, + ); + + _animateTo(targetScale: targetScale, targetOffset: targetOffset); + } + + void _handleDoubleTapDrag(PointerMoveEvent event) { + final anchor = _doubleTapAnchor; + final startLocalPosition = _doubleTapStartLocalPosition; + + if (anchor == null || startLocalPosition == null || _viewportSize == Size.zero) { + return; + } + + final delta = event.localPosition - startLocalPosition; + + if (!_dragZoomActive && delta.distance < _doubleTapDragSlop) { + return; + } + + _dragZoomActive = true; + + final scaleDelta = -delta.dy / 160; + final targetScale = (_doubleTapBaseScale * (1 + scaleDelta)).clamp(widget.minScale, widget.maxScale); + final targetOffset = targetScale <= widget.minScale + _gestureEpsilon + ? Offset.zero + : _anchoredOffsetForScale( + anchor: anchor, + baseOffset: _doubleTapBaseOffset, + baseScale: _doubleTapBaseScale, + targetScale: targetScale, + ); + + setState(() { + _setScale(targetScale); + _offset = targetOffset; + _dismissOffset = Offset.zero; + }); + } + + bool _shouldDismiss() { + if (!widget.dismissible || widget.onDismiss == null || _viewportSize == Size.zero) { + return false; + } + + final threshold = math.min(_viewportSize.shortestSide * 0.18, 120.0); + return _dismissOffset.distance > threshold; + } + + void _handlePointerDown(PointerDownEvent event) { + _activePointers += 1; + + if (_activePointers != 1) { + _singleTapTimer?.cancel(); + _primaryTapPointer = null; + _tapMoved = true; + return; + } + + final now = DateTime.now(); + final isSecondTap = + _lastTapUpTime != null && now.difference(_lastTapUpTime!) <= _doubleTapTimeout && _lastTapUpPosition != null && (_lastTapUpPosition! - event.localPosition).distance <= _doubleTapSlop; + + if (isSecondTap) { + _singleTapTimer?.cancel(); + _doubleTapPointer = event.pointer; + _doubleTapAnchor = event.localPosition; + _doubleTapBaseScale = _scale; + _doubleTapBaseOffset = _offset; + _doubleTapStartLocalPosition = event.localPosition; + _dragZoomActive = false; + _ignoreScaleEnd = true; + _primaryTapPointer = null; + _tapMoved = true; + return; + } + + _primaryTapPointer = event.pointer; + _primaryDownPosition = event.localPosition; + _tapMoved = false; + } + + void _handlePointerMove(PointerMoveEvent event) { + if (_primaryTapPointer == event.pointer && _primaryDownPosition != null) { + final moved = (_primaryDownPosition! - event.localPosition).distance; + if (moved > _doubleTapDragSlop) { + _tapMoved = true; + } + } + + if (_doubleTapPointer == event.pointer) { + _handleDoubleTapDrag(event); + } + } + + void _handlePointerUp(PointerUpEvent event) { + final wasDoubleTapPointer = _doubleTapPointer == event.pointer; + final wasPrimaryTapPointer = _primaryTapPointer == event.pointer; + + if (wasDoubleTapPointer) { + if (_dragZoomActive) { + _snapBackIntoBounds(); + } else if (_doubleTapAnchor != null) { + _handleDoubleTap(_doubleTapAnchor!); + } + + _doubleTapPointer = null; + _doubleTapAnchor = null; + _doubleTapStartLocalPosition = null; + _dragZoomActive = false; + _lastTapUpTime = null; + _lastTapUpPosition = null; + } else if (wasPrimaryTapPointer && !_tapMoved && !_didTriggerLongPress) { + _lastTapUpTime = DateTime.now(); + _lastTapUpPosition = event.localPosition; + _singleTapTimer?.cancel(); + _singleTapTimer = Timer(_doubleTapTimeout, () { + if (!mounted) return; + widget.onTap?.call(); + _lastTapUpTime = null; + _lastTapUpPosition = null; + }); + } + + if (wasPrimaryTapPointer) { + _didTriggerLongPress = false; + } + + _primaryTapPointer = wasPrimaryTapPointer ? null : _primaryTapPointer; + _primaryDownPosition = wasPrimaryTapPointer ? null : _primaryDownPosition; + _activePointers = math.max(0, _activePointers - 1); + } + + void _handlePointerCancel(PointerCancelEvent event) { + if (_doubleTapPointer == event.pointer) { + _doubleTapPointer = null; + _doubleTapAnchor = null; + _doubleTapStartLocalPosition = null; + _dragZoomActive = false; + _ignoreScaleEnd = false; + } + + if (_primaryTapPointer == event.pointer) { + _primaryTapPointer = null; + _primaryDownPosition = null; + _tapMoved = true; + } + + _activePointers = math.max(0, _activePointers - 1); + } + + void _handleScaleStart(ScaleStartDetails details) { + if (_dragZoomActive) return; + + _transformAnimationController.stop(); + _gestureStartScale = _scale; + _gestureStartOffset = _offset; + _gestureStartFocalPoint = details.localFocalPoint; + _gestureMode = (_activePointers > 1 || _scale > widget.minScale + _gestureEpsilon) ? _ViewerGestureMode.transform : _ViewerGestureMode.dismiss; + } + + void _handleScaleUpdate(ScaleUpdateDetails details) { + if (_dragZoomActive || _viewportSize == Size.zero) return; + + final focalPoint = details.localFocalPoint; + final gestureStartFocalPoint = _gestureStartFocalPoint; + + if (gestureStartFocalPoint == null) return; + + final isTransformGesture = _activePointers > 1 || _gestureMode == _ViewerGestureMode.transform || _scale > widget.minScale + _gestureEpsilon || details.scale != 1.0; + + if (isTransformGesture) { + _gestureMode = _ViewerGestureMode.transform; + final nextScale = (_gestureStartScale * details.scale).clamp(widget.minScale, widget.maxScale); + final nextOffset = nextScale <= widget.minScale + _gestureEpsilon + ? Offset.zero + : _anchoredOffsetForScale( + anchor: focalPoint, + baseOffset: _gestureStartOffset, + baseScale: _gestureStartScale, + targetScale: nextScale, + ); + + setState(() { + _setScale(nextScale); + _offset = nextOffset; + _dismissOffset = Offset.zero; + }); + return; + } + + if (!widget.dismissible) return; + + setState(() { + _gestureMode = _ViewerGestureMode.dismiss; + _dismissOffset = focalPoint - gestureStartFocalPoint; + }); + } + + void _handleScaleEnd(ScaleEndDetails details) { + if (_ignoreScaleEnd) { + _ignoreScaleEnd = false; + _gestureMode = _ViewerGestureMode.idle; + return; + } + + if (_dragZoomActive) return; + + if (_gestureMode == _ViewerGestureMode.dismiss) { + if (_shouldDismiss()) { + widget.onDismiss?.call(); + } else { + _animateTo(targetScale: _scale, targetOffset: _offset); + } + } else { + _snapBackIntoBounds(); + } + + _gestureMode = _ViewerGestureMode.idle; + } + + Widget _buildDefaultLoader(BuildContext context) { + return const Center(child: CircularProgressIndicator()); + } + + Widget _buildDefaultError(BuildContext context, Object error) { + return const Center(child: Icon(Icons.broken_image_outlined, color: Colors.white70, size: 36)); + } + + Widget _buildImageWidget() { + final source = widget.source; + + if (source is ThunderImageViewerMemorySource) { + return Image.memory( + source.bytes, + filterQuality: widget.filterQuality, + fit: BoxFit.contain, + semanticLabel: widget.semanticLabel, + ); + } + + final networkSource = source as ThunderImageViewerNetworkSource; + final isAvifByUrl = networkSource.url.toLowerCase().endsWith('.avif'); + final isAvifByContentType = source.contentType?.toLowerCase() == 'image/avif'; + final isAvif = isAvifByUrl || isAvifByContentType; + + if (isAvif) { + return CachedNetworkAvifImage( + networkSource.url, + fit: BoxFit.contain, + filterQuality: widget.filterQuality, + errorBuilder: (context, error, stackTrace) { + return widget.errorBuilder?.call(context, error) ?? _buildDefaultError(context, error); + }, + ); + } + + return CachedNetworkImage( + imageUrl: networkSource.url, + fadeInDuration: const Duration(milliseconds: 100), + fadeOutDuration: Duration.zero, + fit: BoxFit.contain, + imageBuilder: (context, imageProvider) { + return Image( + image: imageProvider, + filterQuality: widget.filterQuality, + fit: BoxFit.contain, + semanticLabel: widget.semanticLabel, + ); + }, + placeholder: (context, url) => widget.loadingBuilder?.call(context) ?? _buildDefaultLoader(context), + errorWidget: (context, url, error) { + return widget.errorBuilder?.call(context, error) ?? _buildDefaultError(context, error); + }, + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + _viewportSize = Size(constraints.maxWidth, constraints.maxHeight); + _baseContentSize = _resolveBaseContentSize(_viewportSize); + + final dismissProgress = _viewportSize == Size.zero ? 0.0 : (_dismissOffset.distance / (_viewportSize.shortestSide * 0.35)).clamp(0.0, 1.0); + final backgroundOpacity = 1.0 - (dismissProgress * 0.75); + final imageOpacity = 1.0 - (dismissProgress * 0.35); + final visualOffset = (_scale <= widget.minScale + _gestureEpsilon ? Offset.zero : _offset) + _dismissOffset; + + return ColoredBox( + color: Color.lerp(Colors.transparent, widget.backgroundColor, backgroundOpacity) ?? widget.backgroundColor, + child: Listener( + behavior: HitTestBehavior.opaque, + onPointerCancel: _handlePointerCancel, + onPointerDown: _handlePointerDown, + onPointerMove: _handlePointerMove, + onPointerUp: _handlePointerUp, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onLongPress: widget.onLongPress == null ? null : _handleLongPress, + onScaleEnd: _handleScaleEnd, + onScaleStart: _handleScaleStart, + onScaleUpdate: _handleScaleUpdate, + child: ClipRect( + child: Center( + child: Opacity( + opacity: imageOpacity, + child: Transform.translate( + offset: visualOffset, + child: Transform.scale( + alignment: Alignment.center, + scale: _scale, + child: RepaintBoundary( + child: SizedBox( + height: _baseContentSize.height, + width: _baseContentSize.width, + child: _buildImageWidget(), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/packages/ui/ui.dart b/lib/packages/ui/ui.dart index 511ca1abe..a468b1f74 100644 --- a/lib/packages/ui/ui.dart +++ b/lib/packages/ui/ui.dart @@ -15,6 +15,7 @@ export 'src/widgets/identity/scalable_text.dart'; export 'src/widgets/layout/conditional_parent_widget.dart'; export 'src/widgets/layout/thunder_bottom_sheet.dart'; export 'src/widgets/layout/thunder_divider.dart'; +export 'src/widgets/media/thunder_image_viewer.dart'; export 'src/widgets/pickers/bottom_sheet_list_picker.dart'; export 'src/widgets/pickers/multi_picker_item.dart'; export 'src/widgets/pickers/picker_item.dart'; diff --git a/lib/src/app/bootstrap/bootstrap.dart b/lib/src/app/bootstrap/bootstrap.dart index f5bd9d231..530273ea9 100644 --- a/lib/src/app/bootstrap/bootstrap.dart +++ b/lib/src/app/bootstrap/bootstrap.dart @@ -10,16 +10,16 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/app/wiring/state_factories.dart'; import 'package:thunder/src/app/shell/thunder_app.dart'; -import 'package:thunder/src/app/bootstrap/preferences_migration.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; -import 'package:thunder/src/features/account/api.dart'; +import 'package:thunder/src/features/session/api.dart'; +/// Initializes Thunder, including setting up the database, user preferences, and session management. Future bootstrap() async { WidgetsFlutterBinding.ensureInitialized(); try { - // Fixes an issue with older Android devices connecting to instances with LetsEncrypt certificates + // Fixes an issue with older Android devices connecting to instances with LetsEncrypt certificates. // https://github.com/thunder-app/thunder/pull/1675 final certificate = await PlatformAssetBundle().load('assets/ca/isrgrootx1.pem'); SecurityContext.defaultContext.setTrustedCertificatesBytes(certificate.buffer.asUint8List()); @@ -36,11 +36,9 @@ Future bootstrap() async { await UserPreferences.instance.initialize(); await performSharedPreferencesMigration(); - final account = await fetchActiveProfile(); - runApp( - BlocProvider( - create: (context) => createProfileBloc(account)..add(InitializeAuth()), + BlocProvider( + create: (context) => createSessionBloc()..add(const SessionInitialized()), child: const ThunderApp(), ), ); diff --git a/lib/src/app/share/share_intent_handler.dart b/lib/src/app/intent/share_intent_handler.dart similarity index 100% rename from lib/src/app/share/share_intent_handler.dart rename to lib/src/app/intent/share_intent_handler.dart diff --git a/lib/src/app/shell/navigation/link_navigation_utils.dart b/lib/src/app/shell/navigation/link_navigation_utils.dart index 2a58f0ddd..b44d141b9 100644 --- a/lib/src/app/shell/navigation/link_navigation_utils.dart +++ b/lib/src/app/shell/navigation/link_navigation_utils.dart @@ -6,11 +6,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:youtube_player_flutter/youtube_player_flutter.dart'; import 'package:intl/message_format.dart'; -import 'package:thunder/src/features/account/api.dart'; -import 'package:thunder/src/features/community/api.dart'; import 'package:url_launcher/url_launcher.dart' as url_launcher; import 'package:flutter_custom_tabs/flutter_custom_tabs.dart'; +import 'package:thunder/src/features/community/api.dart'; import 'package:thunder/src/features/comment/api.dart'; import 'package:thunder/src/features/post/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; @@ -22,11 +21,12 @@ import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/features/instance/data/constants/known_instances.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/shared/media/widgets/video_player.dart'; import 'package:thunder/src/features/user/api.dart'; void _openLink(BuildContext context, {required String url, bool isVideo = false}) async { - final thunderPreferences = context.read().state; + final thunderPreferences = context.read().state; final browserMode = thunderPreferences.browserMode; final openInReaderMode = thunderPreferences.openInReaderMode; @@ -128,7 +128,7 @@ void _showVideoPlayer(BuildContext context, {required String url, int? postId}) /// Attempts to perform in-app navigtion to communities, users, posts, and comments /// Before falling back to opening in the browser (either Custom Tabs or system browser, as specified by the user). void handleLink(BuildContext context, {required String url, bool forceOpenInBrowser = false}) async { - final account = context.read().state.account; + final account = resolveEffectiveAccount(context); // Try navigating to community String? communityName = await getLemmyCommunity(url); @@ -273,7 +273,7 @@ Future _testValidCommunity(BuildContext context, String link, String commu // Since this may take a while, show a loading page. showLoadingPage(context); - final account = context.read().state.account; + final account = resolveEffectiveAccount(context); await CommunityRepositoryImpl(account: account).getCommunity(name: communityName); return true; } catch (e) { @@ -302,7 +302,7 @@ Future _testValidUser(BuildContext context, String link, String userName, // Since this may take a while, show a loading page. showLoadingPage(context); - final account = context.read().state.account; + final account = resolveEffectiveAccount(context); await UserRepositoryImpl(account: account).getUser(username: userName); return true; } catch (e) { diff --git a/lib/src/app/shell/navigation/loading_page.dart b/lib/src/app/shell/navigation/loading_page.dart index 1c5bdf0db..1a44841e5 100644 --- a/lib/src/app/shell/navigation/loading_page.dart +++ b/lib/src/app/shell/navigation/loading_page.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/features/settings/api.dart'; @@ -66,7 +67,7 @@ void showLoadingPage(BuildContext context) { canSwipe: false, builder: (context) => MultiBlocProvider( providers: [ - BlocProvider.value(value: context.read()), + BlocProvider.value(value: context.read()), ], child: PopScope( onPopInvokedWithResult: (didPop, result) => isLoadingPageShown = !didPop, diff --git a/lib/src/app/shell/navigation/navigation_feed.dart b/lib/src/app/shell/navigation/navigation_feed.dart index 5b5b32223..292f20a20 100644 --- a/lib/src/app/shell/navigation/navigation_feed.dart +++ b/lib/src/app/shell/navigation/navigation_feed.dart @@ -7,9 +7,10 @@ part of 'navigation_utils.dart'; /// If [feedType] is [FeedType.community], one of [communityId] or [communityName] must be provided /// If [feedType] is [FeedType.user], one of [userId] or [username] must be provided /// -/// The [context] parameter should contain the following blocs within its widget tree: [AccountBloc], [AuthBloc], [ThunderBloc] +/// The [context] parameter should contain the following blocs within its widget tree: [AccountBloc], [AuthBloc], [ThunderCubit] Future navigateToFeedPage( BuildContext context, { + Account? account, required FeedType feedType, FeedListType? feedListType, PostSortType? postSortType, @@ -18,25 +19,22 @@ Future navigateToFeedPage( String? username, int? userId, }) async { - // Push navigation - ProfileBloc profileBloc = context.read(); - ThunderBloc thunderBloc = context.read(); + final routeScope = resolveAccountAwareRouteScope(context, account: account, includeThunderCubit: true); + final effectiveAccount = routeScope.account; final gestureCubit = context.read(); final themeCubit = context.read(); final feedCubit = context.read(); - AnonymousSubscriptionsBloc anonymousSubscriptionsBloc = context.read(); + final anonymousSubscriptionsCubit = fetchAnonymousSubscriptionsCubit(context); final bool reduceAnimations = themeCubit.state.reduceAnimations; + final defaultPostSortType = postSortType ?? routeScope.profileBloc?.state.siteResponse?.myUser?.localUserView.localUser.defaultSortType ?? feedCubit.state.defaultPostSortType; if (feedType == FeedType.general) { return context.read().add( FeedFetchedEvent( feedType: feedType, feedListType: feedListType, - postSortType: postSortType ?? - (profileBloc.state.siteResponse?.myUser?.localUserView.localUser.defaultSortType != null - ? profileBloc.state.siteResponse!.myUser!.localUserView.localUser.defaultSortType - : feedCubit.state.defaultPostSortType), + postSortType: defaultPostSortType, communityId: communityId, communityName: communityName, userId: userId, @@ -56,20 +54,22 @@ Future navigateToFeedPage( reverseTransitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : const Duration(milliseconds: 500), backGestureDetectionWidth: 45, canSwipe: !kIsWeb && Platform.isIOS || gestureCubit.state.enableFullScreenSwipeNavigationGesture, - canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: profileBloc.state.isLoggedIn, state: gestureCubit.state, isFeedPage: true) || !gestureCubit.state.enableFullScreenSwipeNavigationGesture, + canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: !effectiveAccount.anonymous, state: gestureCubit.state, isFeedPage: true) || !gestureCubit.state.enableFullScreenSwipeNavigationGesture, builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: profileBloc), - BlocProvider.value(value: thunderBloc), - BlocProvider.value(value: anonymousSubscriptionsBloc), - ], + providers: routeScope.providers( + provideThunderCubit: true, + extraProviders: [ + if (anonymousSubscriptionsCubit != null) + BlocProvider.value(value: anonymousSubscriptionsCubit) + else + BlocProvider(create: (_) => AnonymousSubscriptionsCubit()..loadSubscribedCommunities()), + ], + ), child: Material( child: FeedPage( + account: effectiveAccount, feedType: feedType, - postSortType: postSortType ?? - (profileBloc.state.siteResponse?.myUser?.localUserView.localUser.defaultSortType != null - ? profileBloc.state.siteResponse!.myUser!.localUserView.localUser.defaultSortType - : feedCubit.state.defaultPostSortType), + postSortType: defaultPostSortType, communityName: communityName, communityId: communityId, userId: userId, @@ -86,20 +86,20 @@ Future navigateToFeedPage( /// Navigates to the search page /// -/// The [context] parameter should contain the following blocs within its widget tree: [FeedBloc], [ThunderBloc] +/// The [context] parameter should contain the following blocs within its widget tree: [FeedBloc], [ThunderCubit] void navigateToSearchPage(BuildContext context) { final hasFeedBloc = context.findAncestorWidgetOfExactType>() != null; assert(hasFeedBloc == true); final feedBloc = context.read(); - final thunderBloc = context.read(); + final routeScope = resolveAccountAwareRouteScope(context, includeThunderCubit: true); final gestureCubit = context.read(); final themeCubit = context.read(); final reduceAnimations = themeCubit.state.reduceAnimations; final enableFullScreenSwipeNavigationGesture = gestureCubit.state.enableFullScreenSwipeNavigationGesture; - final account = context.read().state.account; + final account = routeScope.account; Navigator.of(context).push( SwipeablePageRoute( @@ -107,11 +107,13 @@ void navigateToSearchPage(BuildContext context) { canSwipe: !kIsWeb && Platform.isIOS || enableFullScreenSwipeNavigationGesture, canOnlySwipeFromEdge: true, builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider(create: (context) => createSearchBloc(account)), - BlocProvider.value(value: thunderBloc), - ], - child: SearchPage(community: feedBloc.state.community), + providers: routeScope.providers( + provideThunderCubit: true, + extraProviders: [ + BlocProvider(create: (context) => createSearchBloc(account)), + ], + ), + child: SearchPage(account: account, community: feedBloc.state.community), ), ), ); diff --git a/lib/src/app/shell/navigation/navigation_instance.dart b/lib/src/app/shell/navigation/navigation_instance.dart index 4ebdf0bd8..2526d1421 100644 --- a/lib/src/app/shell/navigation/navigation_instance.dart +++ b/lib/src/app/shell/navigation/navigation_instance.dart @@ -6,6 +6,7 @@ part of 'navigation_utils.dart'; /// the id of the navigated instance from the original instance (e.g., lemmy.ml's instance id from lemmy.world). Future navigateToInstancePage( BuildContext context, { + Account? account, required String instanceHost, required int? instanceId, }) async { @@ -27,6 +28,10 @@ Future navigateToInstancePage( // Continue if we can't get the site } + final fallbackAccount = Account(id: '', index: -1, anonymous: true, instance: instanceHost, platform: platform); + final routeScope = resolveAccountAwareRouteScope(context, account: account, fallbackAccount: fallbackAccount, useActiveAccount: true, includeThunderCubit: true); + final effectiveAccount = routeScope.account; + final route = SwipeablePageRoute( transitionDuration: isLoadingPageShown ? Duration.zero @@ -36,9 +41,10 @@ Future navigateToInstancePage( reverseTransitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : const Duration(milliseconds: 500), canSwipe: !kIsWeb && Platform.isIOS || enableFullScreenSwipeNavigationGesture, canOnlySwipeFromEdge: true, - builder: (context) => BlocProvider.value( - value: context.read(), + builder: (_) => MultiBlocProvider( + providers: routeScope.providers(provideThunderCubit: true), child: InstancePage( + account: effectiveAccount, instance: ThunderInstanceInfo( id: instanceId, domain: site!.site.actorId, @@ -79,13 +85,14 @@ Future navigateToModlogPage( int? moderatorId, int? commentId, required String subtitle, + Account? account, }) async { - final thunderBloc = context.read(); - final account = context.read().state.account; + final routeScope = resolveAccountAwareRouteScope(context, account: account, includeThunderCubit: true); + final effectiveAccount = routeScope.account; // Optional blocs final hasFeedBloc = context.findAncestorWidgetOfExactType>(); - final feedBloc = hasFeedBloc != null ? context.read() : createFeedBloc(account); + final feedBloc = hasFeedBloc != null ? context.read() : createFeedBloc(effectiveAccount); final gestureCubit = context.read(); final themeCubit = context.read(); @@ -102,11 +109,14 @@ Future navigateToModlogPage( canSwipe: !kIsWeb && Platform.isIOS || enableFullScreenSwipeNavigationGesture, canOnlySwipeFromEdge: true, builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: feedBloc), - BlocProvider.value(value: thunderBloc), - ], + providers: routeScope.providers( + provideThunderCubit: true, + extraProviders: [ + BlocProvider.value(value: feedBloc), + ], + ), child: ModlogFeedPage( + account: effectiveAccount, modlogActionType: modlogActionType, communityId: communityId, userId: userId, diff --git a/lib/src/app/shell/navigation/navigation_misc.dart b/lib/src/app/shell/navigation/navigation_misc.dart index 8bfc994d0..8b71b10f9 100644 --- a/lib/src/app/shell/navigation/navigation_misc.dart +++ b/lib/src/app/shell/navigation/navigation_misc.dart @@ -2,13 +2,13 @@ part of 'navigation_utils.dart'; /// Navigates to the [ReportFeedPage] page. /// -/// The [context] parameter should contain the following blocs within its widget tree: [FeedBloc], [ThunderBloc] +/// The [context] parameter should contain the following blocs within its widget tree: [FeedBloc], [ThunderCubit] void navigateToReportPage(BuildContext context) { final hasFeedBloc = context.findAncestorWidgetOfExactType>() != null; assert(hasFeedBloc == true); + final routeScope = resolveAccountAwareRouteScope(context, useActiveAccount: true, includeThunderCubit: true); final feedBloc = context.read(); - final thunderBloc = context.read(); final gestureCubit = context.read(); final themeCubit = context.read(); @@ -22,10 +22,12 @@ void navigateToReportPage(BuildContext context) { canOnlySwipeFromEdge: true, builder: (_) { return MultiBlocProvider( - providers: [ - BlocProvider.value(value: feedBloc), - BlocProvider.value(value: thunderBloc), - ], + providers: routeScope.providers( + provideThunderCubit: true, + extraProviders: [ + BlocProvider.value(value: feedBloc), + ], + ), child: const ReportFeedPage(), ); }, diff --git a/lib/src/app/shell/navigation/navigation_notification.dart b/lib/src/app/shell/navigation/navigation_notification.dart index 6ecf501ea..0bde019dc 100644 --- a/lib/src/app/shell/navigation/navigation_notification.dart +++ b/lib/src/app/shell/navigation/navigation_notification.dart @@ -15,7 +15,7 @@ void navigateToNotificationPage( // It can take a little while to set up notifications, so show a loading page showLoadingPage(context); - final thunderBloc = context.read(); + final thunderBloc = context.read(); final gestureCubit = context.read(); final themeCubit = context.read(); final reduceAnimations = themeCubit.state.reduceAnimations; diff --git a/lib/src/app/shell/navigation/navigation_post.dart b/lib/src/app/shell/navigation/navigation_post.dart index cd4c62ff2..d748941e1 100644 --- a/lib/src/app/shell/navigation/navigation_post.dart +++ b/lib/src/app/shell/navigation/navigation_post.dart @@ -1,6 +1,6 @@ part of 'navigation_utils.dart'; -({String postApId, post_bloc.PostBloc postBloc})? _cachedPostBloc; +({String accountId, String postApId, post_bloc.PostBloc postBloc})? _cachedPostBloc; /// Navigates to the post page with the given [post] or [postId]. /// @@ -8,6 +8,7 @@ part of 'navigation_utils.dart'; /// Otherwise, the post page will fetch the post with the given [postId]. Future navigateToPost( BuildContext context, { + Account? account, int? postId, ThunderPost? post, int? highlightedCommentId, @@ -15,9 +16,8 @@ Future navigateToPost( }) async { assert((postId != null || post != null), 'One of the parameters must be provided'); - // Required blocs - final profileBloc = context.read(); - final thunderBloc = context.read(); + final routeScope = resolveAccountAwareRouteScope(context, account: account, includeThunderCubit: true); + final effectiveAccount = routeScope.account; // Optional blocs final hasFeedBloc = context.findAncestorWidgetOfExactType>(); @@ -25,15 +25,13 @@ Future navigateToPost( ThunderPost? pvm = post; - final account = context.read().state.account; - if (pvm == null) { - final response = await PostRepositoryImpl(account: account).getPost(postId!); + final response = await PostRepositoryImpl(account: effectiveAccount).getPost(postId!); pvm = response?['post']; } // Mark post as read when tapped - if (profileBloc.state.isLoggedIn) { + if (!effectiveAccount.anonymous) { feedBloc?.add(FeedItemActionedEvent(postId: pvm!.id, postAction: PostAction.read, actionInput: const ReadPostInput(true))); } @@ -42,11 +40,12 @@ Future navigateToPost( final reduceAnimations = themeCubit.state.reduceAnimations; final enableFullScreenSwipeNavigationGesture = gestureCubit.state.enableFullScreenSwipeNavigationGesture; - final post_bloc.PostBloc postBloc = _cachedPostBloc?.postApId == pvm!.apId + final post_bloc.PostBloc postBloc = _cachedPostBloc?.accountId == effectiveAccount.id && _cachedPostBloc?.postApId == pvm!.apId ? _cachedPostBloc!.postBloc : (_cachedPostBloc = ( - postApId: pvm.apId, - postBloc: createPostBloc(account), + accountId: effectiveAccount.id, + postApId: pvm!.apId, + postBloc: createPostBloc(effectiveAccount), )) .postBloc; @@ -60,7 +59,7 @@ Future navigateToPost( backGestureDetectionStartOffset: !kIsWeb && Platform.isAndroid ? 45 : 0, backGestureDetectionWidth: 45, canSwipe: !kIsWeb && Platform.isIOS || enableFullScreenSwipeNavigationGesture, - canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: profileBloc.state.isLoggedIn, state: gestureCubit.state, isPostPage: true) || !enableFullScreenSwipeNavigationGesture, + canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: !effectiveAccount.anonymous, state: gestureCubit.state, isPostPage: true) || !enableFullScreenSwipeNavigationGesture, builder: (_) { final postNavigationCubit = PostNavigationCubit(); if (highlightedCommentId != null) { @@ -68,13 +67,14 @@ Future navigateToPost( } return MultiBlocProvider( - providers: [ - BlocProvider.value(value: profileBloc), - BlocProvider.value(value: thunderBloc), - BlocProvider.value(value: postBloc), - BlocProvider.value(value: postNavigationCubit), - BlocProvider(create: (context) => AnonymousSubscriptionsBloc()), - ], + providers: routeScope.providers( + provideThunderCubit: true, + extraProviders: [ + BlocProvider.value(value: postBloc), + BlocProvider.value(value: postNavigationCubit), + BlocProvider(create: (context) => AnonymousSubscriptionsCubit()..loadSubscribedCommunities()), + ], + ), child: PostPage( initialPost: postBloc.state.post ?? pvm!, highlightedCommentId: highlightedCommentId, @@ -98,11 +98,10 @@ Future getPostFromComment(ThunderComment comment, Account account) } Future navigateToComment(BuildContext context, ThunderComment comment) async { - final profileBloc = context.read(); - final thunderBloc = context.read(); + final routeScope = resolveAccountAwareRouteScope(context, includeThunderCubit: true); final gestureCubit = context.read(); - final account = context.read().state.account; + final effectiveAccount = routeScope.account; final themeCubit = context.read(); final reduceAnimations = themeCubit.state.reduceAnimations; @@ -116,20 +115,21 @@ Future navigateToComment(BuildContext context, ThunderComment comment) asy reverseTransitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : const Duration(milliseconds: 500), backGestureDetectionWidth: 45, canSwipe: !kIsWeb && Platform.isIOS || gestureCubit.state.enableFullScreenSwipeNavigationGesture, - canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: profileBloc.state.isLoggedIn, state: gestureCubit.state, isPostPage: true) || !gestureCubit.state.enableFullScreenSwipeNavigationGesture, + canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: !effectiveAccount.anonymous, state: gestureCubit.state, isPostPage: true) || !gestureCubit.state.enableFullScreenSwipeNavigationGesture, builder: (context) { final postNavigationCubit = PostNavigationCubit(); postNavigationCubit.setHighlightedCommentId(comment.id); return MultiBlocProvider( - providers: [ - BlocProvider.value(value: profileBloc), - BlocProvider.value(value: thunderBloc), - BlocProvider(create: (context) => createPostBloc(account)), - BlocProvider.value(value: postNavigationCubit), - ], + providers: routeScope.providers( + provideThunderCubit: true, + extraProviders: [ + BlocProvider(create: (context) => createPostBloc(effectiveAccount)), + BlocProvider.value(value: postNavigationCubit), + ], + ), child: FutureBuilder( - future: getPostFromComment(comment, account), + future: getPostFromComment(comment, effectiveAccount), builder: (context, snapshot) { if (snapshot.hasData) { return PostPage( @@ -161,8 +161,9 @@ Future navigateToCreateCommentPage( assert(!(post == null && parentComment == null && comment == null)); assert(!(post != null && (parentComment != null || comment != null))); - final profileBloc = context.read(); - final thunderBloc = context.read(); + final routeScope = resolveAccountAwareRouteScope(context, account: account, includeThunderCubit: true); + final effectiveAccount = routeScope.account; + final createCommentCubit = createCreateCommentCubit(effectiveAccount); final gestureCubit = context.read(); final themeCubit = context.read(); @@ -179,12 +180,14 @@ Future navigateToCreateCommentPage( canSwipe: !kIsWeb && Platform.isIOS || enableFullScreenSwipeNavigationGesture, canOnlySwipeFromEdge: true, builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: thunderBloc), - BlocProvider.value(value: profileBloc), - ], + providers: routeScope.providers( + provideThunderCubit: true, + extraProviders: [ + BlocProvider.value(value: createCommentCubit), + ], + ), child: CreateCommentPage( - account: account ?? profileBloc.state.account, + account: effectiveAccount, post: post, comment: comment, parentComment: parentComment, @@ -214,12 +217,11 @@ Future navigateToCreatePostPage( }) async { try { final l10n = AppLocalizations.of(context)!; - final effectiveAccount = account ?? context.read().state.account; + final routeScope = resolveAccountAwareRouteScope(context, account: account, includeThunderCubit: true); + final effectiveAccount = routeScope.account; FeedBloc? feedBloc; PostBloc? postBloc; - ThunderBloc thunderBloc = context.read(); - ProfileBloc profileBloc = context.read(); CreatePostCubit createPostCubit = createCreatePostCubit(effectiveAccount); final themeCubit = context.read(); @@ -253,10 +255,9 @@ Future navigateToCreatePostPage( builder: (navigatorContext) { return MultiBlocProvider( providers: [ - feedBloc != null ? BlocProvider.value(value: feedBloc) : BlocProvider(create: (context) => createFeedBloc(effectiveAccount)), + feedBloc != null ? BlocProvider.value(value: feedBloc) : BlocProvider(create: (context) => createFeedBloc(effectiveAccount)), if (postBloc != null) BlocProvider.value(value: postBloc), - BlocProvider.value(value: thunderBloc), - BlocProvider.value(value: profileBloc), + ...routeScope.providers(provideThunderCubit: true), BlocProvider.value(value: createPostCubit), ], child: CreatePostPage( @@ -286,7 +287,7 @@ Future navigateToCreatePostPage( l10n.postCreatedSuccessfully, trailingIcon: Icons.remove_red_eye_rounded, trailingAction: () { - navigateToPost(context, post: updatedPost); + navigateToPost(context, account: effectiveAccount, post: updatedPost); }, ); } catch (e) { diff --git a/lib/src/app/shell/navigation/navigation_settings.dart b/lib/src/app/shell/navigation/navigation_settings.dart index c790e6796..4dcfe0f5e 100644 --- a/lib/src/app/shell/navigation/navigation_settings.dart +++ b/lib/src/app/shell/navigation/navigation_settings.dart @@ -4,16 +4,14 @@ part of 'navigation_utils.dart'; /// /// Additionally, the [settingToHighlight] parameter can be used to highlight a specific setting when the page is opened. void navigateToSettingPage(BuildContext context, LocalSettings setting, {LocalSettings? settingToHighlight}) { - final thunderBloc = context.read(); - final profileBloc = context.read(); + final routeScope = resolveAccountAwareRouteScope(context, useActiveAccount: true, includeThunderCubit: true); + final account = routeScope.account; final gestureCubit = context.read(); final themeCubit = context.read(); final reduceAnimations = themeCubit.state.reduceAnimations; final enableFullScreenSwipeNavigationGesture = gestureCubit.state.enableFullScreenSwipeNavigationGesture; - final account = context.read().state.account; - String pageToNav = { LocalSettingsCategories.posts: SETTINGS_APPEARANCE_POSTS_PAGE, LocalSettingsCategories.comments: SETTINGS_APPEARANCE_COMMENTS_PAGE, @@ -42,20 +40,13 @@ void navigateToSettingPage(BuildContext context, LocalSettings setting, {LocalSe canSwipe: !kIsWeb && Platform.isIOS || enableFullScreenSwipeNavigationGesture, canOnlySwipeFromEdge: true, builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: profileBloc), - BlocProvider.value(value: thunderBloc), - ], + providers: routeScope.providers(provideThunderCubit: true), child: AboutSettingsPage(settingToHighlight: settingToHighlight ?? setting), ), ), ); } else if (pageToNav == SETTINGS_ACCOUNT_MEDIA_PAGE) { - final hasUserSettingsBloc = context.findAncestorWidgetOfExactType>() != null; - - final userSettingsBloc = hasUserSettingsBloc ? context.read() : createUserSettingsBloc(account); - - userSettingsBloc.add(const ListMediaEvent()); + final userMediaCubit = createUserMediaCubit(account)..loadMedia(); Navigator.of(context).push( SwipeablePageRoute( @@ -64,16 +55,40 @@ void navigateToSettingPage(BuildContext context, LocalSettings setting, {LocalSe canOnlySwipeFromEdge: true, builder: (context) => MultiBlocProvider( providers: [ - BlocProvider.value(value: thunderBloc), - BlocProvider.value(value: userSettingsBloc), + BlocProvider.value(value: routeScope.thunderCubit!), + BlocProvider(create: (_) => userMediaCubit), ], - child: MediaManagementPage(), + child: MediaManagementPage(account: account), + ), + ), + ); + } else if (pageToNav == SETTINGS_ACCOUNT_BLOCKLIST_PAGE) { + final userBlocksCubit = createUserBlocksCubit(account)..loadBlocks(); + + Navigator.of(context).push( + SwipeablePageRoute( + transitionDuration: reduceAnimations ? const Duration(milliseconds: 100) : null, + canSwipe: !kIsWeb && Platform.isIOS || enableFullScreenSwipeNavigationGesture, + canOnlySwipeFromEdge: true, + builder: (context) => MultiBlocProvider( + providers: routeScope.providers( + provideFeatureAccountCubit: false, + extraProviders: [ + BlocProvider(create: (_) => userBlocksCubit), + ], + ), + child: UserSettingsBlockPage(), ), ), ); } else { - final hasUserSettingsBloc = context.findAncestorWidgetOfExactType>() != null; - final userSettingsBloc = hasUserSettingsBloc ? context.read() : createUserSettingsBloc(account); + final needsAccountSettingsCubit = pageToNav == SETTINGS_ACCOUNT_PAGE || pageToNav == SETTINGS_ACCOUNT_LANGUAGES_PAGE; + final hasAccountSettingsCubit = needsAccountSettingsCubit && context.findAncestorWidgetOfExactType>() != null; + final accountSettingsCubit = !needsAccountSettingsCubit + ? null + : hasAccountSettingsCubit + ? context.read() + : createAccountSettingsCubit(account, initialSiteResponse: routeScope.profileBloc?.state.siteResponse); Navigator.of(context).push( SwipeablePageRoute( @@ -81,11 +96,13 @@ void navigateToSettingPage(BuildContext context, LocalSettings setting, {LocalSe canSwipe: !kIsWeb && Platform.isIOS || enableFullScreenSwipeNavigationGesture, canOnlySwipeFromEdge: true, builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: profileBloc), - BlocProvider.value(value: thunderBloc), - BlocProvider.value(value: userSettingsBloc), - ], + providers: routeScope.providers( + provideThunderCubit: true, + provideFeatureAccountCubit: pageToNav != SETTINGS_ACCOUNT_LANGUAGES_PAGE, + extraProviders: [ + if (accountSettingsCubit != null) BlocProvider.value(value: accountSettingsCubit), + ], + ), child: switch (pageToNav) { SETTINGS_GENERAL_PAGE => GeneralSettingsPage(settingToHighlight: settingToHighlight ?? setting), SETTINGS_APPEARANCE_POSTS_PAGE => PostAppearanceSettingsPage(settingToHighlight: settingToHighlight ?? setting), @@ -95,7 +112,6 @@ void navigateToSettingPage(BuildContext context, LocalSettings setting, {LocalSe SETTINGS_FILTERS_PAGE => FilterSettingsPage(settingToHighlight: settingToHighlight ?? setting), SETTINGS_ACCOUNT_PAGE => UserSettingsPage(settingToHighlight: settingToHighlight ?? setting), SETTINGS_ACCOUNT_LANGUAGES_PAGE => DiscussionLanguageSelector(), - SETTINGS_ACCOUNT_BLOCKLIST_PAGE => UserSettingsBlockPage(), SETTINGS_APPEARANCE_THEMES_PAGE => ThemeSettingsPage(settingToHighlight: settingToHighlight ?? setting), SETTINGS_DEBUG_PAGE => DebugSettingsPage(settingToHighlight: settingToHighlight ?? setting), SETTINGS_VIDEO_PAGE => VideoPlayerSettingsPage(settingToHighlight: settingToHighlight ?? setting), diff --git a/lib/src/app/shell/navigation/navigation_utils.dart b/lib/src/app/shell/navigation/navigation_utils.dart index ffe481ee6..d2b89209b 100644 --- a/lib/src/app/shell/navigation/navigation_utils.dart +++ b/lib/src/app/shell/navigation/navigation_utils.dart @@ -22,6 +22,7 @@ import 'package:thunder/src/features/moderator/moderator.dart'; import 'package:thunder/src/features/modlog/modlog.dart'; import 'package:thunder/src/features/notification/notification.dart'; import 'package:thunder/src/features/search/search.dart'; +import 'package:thunder/src/features/session/session.dart'; import 'package:thunder/src/features/settings/settings.dart'; import 'package:thunder/src/app/shell/navigation/loading_page.dart'; @@ -44,3 +45,59 @@ part 'navigation_misc.dart'; part 'navigation_notification.dart'; part 'navigation_post.dart'; part 'navigation_settings.dart'; + +bool _matchesAccount(Account first, Account second) => first.id == second.id && first.instance == second.instance && first.anonymous == second.anonymous; + +class AccountAwareRouteScope { + const AccountAwareRouteScope._({required this.account, this.profileBloc, this.thunderCubit}); + + final Account account; + final ProfileBloc? profileBloc; + final ThunderCubit? thunderCubit; + + List providers({ + bool provideFeatureAccountCubit = true, + bool provideThunderCubit = false, + List extraProviders = const [], + }) { + return [ + if (profileBloc != null) BlocProvider.value(value: profileBloc!) else BlocProvider(create: (_) => createProfileBloc(account)..add(InitializeAuth())), + if (provideThunderCubit && thunderCubit != null) BlocProvider.value(value: thunderCubit!), + if (provideFeatureAccountCubit) BlocProvider(create: (_) => FeatureAccountCubit(baseAccount: account)), + ...extraProviders, + ]; + } +} + +AccountAwareRouteScope resolveAccountAwareRouteScope( + BuildContext context, { + Account? account, + Account? fallbackAccount, + bool useActiveAccount = false, + bool includeThunderCubit = false, +}) { + final resolvedAccount = account ?? (useActiveAccount ? resolveActiveAccount(context, fallbackAccount: fallbackAccount) : resolveEffectiveAccount(context, fallbackAccount: fallbackAccount)); + + return AccountAwareRouteScope._( + account: resolvedAccount, + profileBloc: fetchProfileBloc(context, resolvedAccount), + thunderCubit: includeThunderCubit ? context.read() : null, + ); +} + +ProfileBloc? fetchProfileBloc(BuildContext context, Account account) { + try { + final profileBloc = context.read(); + return _matchesAccount(profileBloc.state.account, account) ? profileBloc : null; + } catch (_) { + return null; + } +} + +AnonymousSubscriptionsCubit? fetchAnonymousSubscriptionsCubit(BuildContext context) { + try { + return context.read(); + } catch (_) { + return null; + } +} diff --git a/lib/src/app/shell/pages/thunder_page.dart b/lib/src/app/shell/pages/thunder_page.dart index d3e9fbd85..36df859a1 100644 --- a/lib/src/app/shell/pages/thunder_page.dart +++ b/lib/src/app/shell/pages/thunder_page.dart @@ -14,6 +14,7 @@ import 'package:fading_edge_scrollview/fading_edge_scrollview.dart'; // Internal import 'package:thunder/l10n/generated/app_localizations.dart'; +import 'package:thunder/src/app/shell/state/shell_chrome_cubit.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; @@ -21,7 +22,7 @@ import 'package:thunder/src/foundation/persistence/persistence.dart'; import 'package:thunder/src/foundation/utils/check_github_update.dart'; import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/app/shell/routing/deep_link.dart'; -import 'package:thunder/src/app/share/share_intent_handler.dart'; +import 'package:thunder/src/app/intent/share_intent_handler.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; @@ -35,12 +36,15 @@ import 'package:thunder/src/app/shell/widgets/bottom_nav_bar.dart'; import 'package:thunder/src/app/shell/navigation/link_navigation_utils.dart'; import 'package:thunder/src/features/inbox/inbox.dart'; import 'package:thunder/src/features/search/search.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/features/settings/settings.dart'; import 'package:thunder/src/shared/error_message.dart'; +import 'package:thunder/src/app/state/app_bootstrap_cubit/app_bootstrap_cubit.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/packages/ui/ui.dart' show showSnackbar; String? currentIntent; +bool hasAttemptedDraftRestore = false; class Thunder extends StatefulWidget { final PageController pageController; @@ -52,13 +56,14 @@ class Thunder extends StatefulWidget { } class _ThunderState extends State { + final FeedActionController _rootFeedActionController = FeedActionController(); + int selectedPageIndex = 0; int appExitCounter = 0; bool hasShownUpdateDialog = false; bool hasShownChangelogDialog = false; bool hasShownPageView = false; - bool hasAttemptedDraftRestore = false; bool _isFabOpen = false; @@ -265,14 +270,14 @@ class _ThunderState extends State { return shouldListen; }, listener: (context, state) { - final currentAccountId = context.read().state.account.id; + final currentAccountId = context.read().state.activeAccount?.id; final notificationAccountId = state.accountId; // Check if we need to switch accounts first if (notificationAccountId != null && notificationAccountId != currentAccountId) { // Mark as pending navigation and switch accounts context.read().setPending(); - context.read().add(SwitchProfile(accountId: notificationAccountId, reload: true)); + context.read().add(SessionSwitched(sessionKey: notificationAccountId)); return; } @@ -303,8 +308,14 @@ class _ThunderState extends State { } }, ), - BlocListener( - listenWhen: (previous, current) => previous.status != current.status && current.status == ThunderStatus.success, + BlocListener( + listenWhen: (previous, current) => previous.status != current.status && current.status == AppBootstrapStatus.success, + listener: (context, state) { + context.read().reload(); + }, + ), + BlocListener( + listenWhen: (previous, current) => previous != current, listener: (context, state) { // Reload preference cubits when preferences change context.read().reload(); @@ -316,21 +327,21 @@ class _ThunderState extends State { }, ), ], - child: BlocBuilder( + child: BlocBuilder( buildWhen: (previous, current) => previous.status != current.status, - builder: (context, thunderBlocState) { + builder: (context, appBootstrapState) { reduceAnimations = context.read().state.reduceAnimations; + final thunderState = context.watch().state; - switch (thunderBlocState.status) { - case ThunderStatus.initial: - context.read().add(InitializeAppEvent()); + switch (appBootstrapState.status) { + case AppBootstrapStatus.initial: + context.read().initialize(); return Container(); - case ThunderStatus.loading: + case AppBootstrapStatus.loading: return Container(); - case ThunderStatus.refreshing: - case ThunderStatus.success: + case AppBootstrapStatus.success: // Update the variable so that it can be used in _handleBackButtonPress - _isFabOpen = context.read().state.isFeedFabOpen; + _isFabOpen = context.read().state.isFeedFabOpen; return Scaffold( key: scaffoldStateKey, @@ -347,31 +358,32 @@ class _ThunderState extends State { opacity: selectedPageIndex == 0 ? 1.0 : 0.0, duration: const Duration(milliseconds: 150), curve: Curves.easeIn, - child: IgnorePointer(ignoring: selectedPageIndex != 0, child: const FeedFAB()), + child: IgnorePointer(ignoring: selectedPageIndex != 0, child: FeedFAB(actionController: _rootFeedActionController)), ) : null, floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling, bottomNavigationBar: Builder( builder: (context) { final reduceAnimations = context.read().state.reduceAnimations; - final hideBottomBarOnScroll = context.read().state.hideBottomBarOnScroll; + final hideBottomBarOnScroll = thunderState.hideBottomBarOnScroll; return AnimatedSize( duration: Duration(milliseconds: reduceAnimations ? 0 : 200), curve: Curves.easeInOut, clipBehavior: Clip.hardEdge, alignment: Alignment.topCenter, child: SizedBox( - height: (hideBottomBarOnScroll && !context.select((cubit) => cubit.state.isBottomNavBarVisible)) ? 0 : null, + height: (hideBottomBarOnScroll && !context.select((cubit) => cubit.state.isBottomNavBarVisible)) ? 0 : null, child: AnimatedOpacity( duration: Duration(milliseconds: reduceAnimations ? 0 : 150), curve: Curves.easeOut, - opacity: (hideBottomBarOnScroll && !context.select((cubit) => cubit.state.isBottomNavBarVisible)) ? 0.0 : 1.0, + opacity: (hideBottomBarOnScroll && !context.select((cubit) => cubit.state.isBottomNavBarVisible)) ? 0.0 : 1.0, child: CustomBottomNavigationBar( + feedActionController: _rootFeedActionController, selectedPageIndex: selectedPageIndex, onPageChange: (int index) { // Reset bottom nav bar visibility when switching pages - if (hideBottomBarOnScroll && !context.read().state.isBottomNavBarVisible) { - context.read().setBottomNavBarVisible(true); + if (hideBottomBarOnScroll && !context.read().state.isBottomNavBarVisible) { + context.read().setBottomNavBarVisible(true); } setState(() { selectedPageIndex = index; @@ -438,8 +450,8 @@ class _ThunderState extends State { case ProfileStatus.success: _restoreDraftSession(state); - Version? version = thunderBlocState.version; - bool showInAppUpdateNotification = thunderBlocState.showInAppUpdateNotification; + Version? version = appBootstrapState.version; + bool showInAppUpdateNotification = thunderState.showInAppUpdateNotification; if (version?.hasUpdate == true && hasShownUpdateDialog == false && showInAppUpdateNotification == true) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -465,7 +477,7 @@ class _ThunderState extends State { // Immediately update the current version for next time. prefs.setString('current_version', currentVersion); - if (lastKnownVersion != null && lastKnownVersion != currentVersion && thunderBlocState.showUpdateChangelogs) { + if (lastKnownVersion != null && lastKnownVersion != currentVersion && thunderState.showUpdateChangelogs) { final String changelog = await fetchCurrentVersionChangelog(); if (context.mounted) { @@ -546,6 +558,7 @@ class _ThunderState extends State { builder: (context) { final feedCubit = context.read(); return FeedPage( + actionController: _rootFeedActionController, useGlobalFeedBloc: true, feedType: FeedType.general, feedListType: state.siteResponse?.myUser?.localUserView.localUser.defaultListingType ?? feedCubit.state.defaultFeedListType, @@ -556,7 +569,7 @@ class _ThunderState extends State { ); }, ), - const SearchPage(), + SearchPage(account: state.account), const AccountPage(), const InboxPage(), const SettingsPage(), @@ -596,13 +609,13 @@ class _ThunderState extends State { }, ), ); - case ThunderStatus.failure: + case AppBootstrapStatus.failure: return ErrorMessage( - message: thunderBlocState.errorMessage, + message: appBootstrapState.errorMessage, actions: [ ( text: AppLocalizations.of(context)!.refreshContent, - action: () => context.read().add(InitializeAuth()), + action: () => context.read().initialize(), loading: false, ), ], diff --git a/lib/src/app/shell/routing/deep_link.dart b/lib/src/app/shell/routing/deep_link.dart index 36bfd3a56..456f2b701 100644 --- a/lib/src/app/shell/routing/deep_link.dart +++ b/lib/src/app/shell/routing/deep_link.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/features/instance/instance.dart'; import 'package:thunder/src/features/post/post.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/app/shell/routing/deep_link_enums.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; @@ -121,7 +119,7 @@ Future _initializeLemmyClient(BuildContext context) async { while (attempts < maxRetries) { try { - final account = await fetchActiveProfile(); + final account = resolveActiveAccount(context); if (account.instance.isEmpty) { throw DeepLinkException(GlobalContext.l10n.errorNoActiveInstance, type: DeepLinkErrorType.initialization); } @@ -233,7 +231,7 @@ Future _navigateToPost(BuildContext context, String link) async } try { - final account = context.read().state.account; + final account = resolveEffectiveAccount(context); final post = await PostRepositoryImpl(account: account).getPost(postId); if (!context.mounted) { @@ -323,7 +321,7 @@ Future _navigateToComment(BuildContext context, String link) asy if (!context.mounted) { return DeepLinkResult.failure(GlobalContext.l10n.unexpectedError); } - final account = context.read().state.account; + final account = resolveEffectiveAccount(context); final comment = await CommentRepositoryImpl(account: account).getComment(commentId); navigateToComment(context, comment); diff --git a/lib/src/features/feed/application/state/fab_state_cubit.dart b/lib/src/app/shell/state/shell_chrome_cubit.dart similarity index 63% rename from lib/src/features/feed/application/state/fab_state_cubit.dart rename to lib/src/app/shell/state/shell_chrome_cubit.dart index 8f715589b..a4f9936e7 100644 --- a/lib/src/features/feed/application/state/fab_state_cubit.dart +++ b/lib/src/app/shell/state/shell_chrome_cubit.dart @@ -1,48 +1,44 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -part 'fab_state.dart'; +part 'shell_chrome_state.dart'; -/// Cubit for managing FAB's state -class FabStateCubit extends Cubit { - FabStateCubit() : super(const FabStateState()); +/// Root-scoped cubit for shell chrome state. +class ShellChromeCubit extends Cubit { + ShellChromeCubit() : super(const ShellChromeState()); + + void setBottomNavBarVisible(bool isVisible) { + emit(state.copyWith(isBottomNavBarVisible: isVisible)); + } - /// Toggles the feed FAB's open state void toggleFeedFab() { emit(state.copyWith(isFeedFabOpen: !state.isFeedFabOpen)); } - /// Sets the feed FAB's open state void setFeedFabOpen(bool isOpen) { emit(state.copyWith(isFeedFabOpen: isOpen)); } - /// Toggles the feed FAB's summoned/visible state void toggleFeedFabSummoned() { emit(state.copyWith(isFeedFabSummoned: !state.isFeedFabSummoned)); } - /// Sets the feed FAB's summoned/visible state void setFeedFabSummoned(bool isSummoned) { emit(state.copyWith(isFeedFabSummoned: isSummoned)); } - /// Toggles the post FAB's open state void togglePostFab() { emit(state.copyWith(isPostFabOpen: !state.isPostFabOpen)); } - /// Sets the post FAB's open state void setPostFabOpen(bool isOpen) { emit(state.copyWith(isPostFabOpen: isOpen)); } - /// Toggles the post FAB's summoned/visible state void togglePostFabSummoned() { emit(state.copyWith(isPostFabSummoned: !state.isPostFabSummoned)); } - /// Sets the post FAB's summoned/visible state void setPostFabSummoned(bool isSummoned) { emit(state.copyWith(isPostFabSummoned: isSummoned)); } diff --git a/lib/src/features/feed/application/state/fab_state.dart b/lib/src/app/shell/state/shell_chrome_state.dart similarity index 57% rename from lib/src/features/feed/application/state/fab_state.dart rename to lib/src/app/shell/state/shell_chrome_state.dart index 4a1e652cf..9679981b4 100644 --- a/lib/src/features/feed/application/state/fab_state.dart +++ b/lib/src/app/shell/state/shell_chrome_state.dart @@ -1,32 +1,29 @@ -part of 'fab_state_cubit.dart'; +part of 'shell_chrome_cubit.dart'; -class FabStateState extends Equatable { - const FabStateState({ +class ShellChromeState extends Equatable { + const ShellChromeState({ + this.isBottomNavBarVisible = true, this.isFeedFabOpen = false, this.isFeedFabSummoned = true, this.isPostFabOpen = false, this.isPostFabSummoned = true, }); - /// Whether the feed FAB is currently open + final bool isBottomNavBarVisible; final bool isFeedFabOpen; - - /// Whether the feed FAB is currently summoned (visible on screen) final bool isFeedFabSummoned; - - /// Whether the post FAB is currently open final bool isPostFabOpen; - - /// Whether the post FAB is currently summoned (visible on screen) final bool isPostFabSummoned; - FabStateState copyWith({ + ShellChromeState copyWith({ + bool? isBottomNavBarVisible, bool? isFeedFabOpen, bool? isFeedFabSummoned, bool? isPostFabOpen, bool? isPostFabSummoned, }) { - return FabStateState( + return ShellChromeState( + isBottomNavBarVisible: isBottomNavBarVisible ?? this.isBottomNavBarVisible, isFeedFabOpen: isFeedFabOpen ?? this.isFeedFabOpen, isFeedFabSummoned: isFeedFabSummoned ?? this.isFeedFabSummoned, isPostFabOpen: isPostFabOpen ?? this.isPostFabOpen, @@ -35,5 +32,5 @@ class FabStateState extends Equatable { } @override - List get props => [isFeedFabOpen, isFeedFabSummoned, isPostFabOpen, isPostFabSummoned]; + List get props => [isBottomNavBarVisible, isFeedFabOpen, isFeedFabSummoned, isPostFabOpen, isPostFabSummoned]; } diff --git a/lib/src/app/shell/thunder_app.dart b/lib/src/app/shell/thunder_app.dart index 6aa5f893b..97d82769b 100644 --- a/lib/src/app/shell/thunder_app.dart +++ b/lib/src/app/shell/thunder_app.dart @@ -1,18 +1,22 @@ import 'dart:async'; import 'dart:io'; -import 'package:dynamic_color/dynamic_color.dart'; -import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; + +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:l10n_esperanto/l10n_esperanto.dart'; import 'package:overlay_support/overlay_support.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; +import 'package:thunder/src/app/shell/state/shell_chrome_cubit.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; +import 'package:thunder/src/app/shell/widgets/session.dart'; +import 'package:thunder/src/app/shell/widgets/session_scope.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/comment/api.dart'; import 'package:thunder/src/features/feed/api.dart'; @@ -22,8 +26,6 @@ import 'package:thunder/src/foundation/contracts/contracts.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; import 'package:thunder/src/features/community/api.dart'; -import 'package:thunder/src/features/account/api.dart'; -import 'package:thunder/src/features/inbox/api.dart'; import 'package:thunder/src/features/notification/api.dart'; import 'package:thunder/src/features/settings/domain/models/language_local.dart'; @@ -35,9 +37,9 @@ class ThunderApp extends StatefulWidget { } class _ThunderAppState extends State { - final StreamController notificationsStreamController = StreamController(); + final notificationsStreamController = StreamController(); - PageController thunderPageController = PageController(initialPage: 0); + final thunderPageController = PageController(initialPage: 0); @override void initState() { @@ -72,22 +74,21 @@ class _ThunderAppState extends State { providers: [ BlocProvider(create: (context) => createDeepLinksCubit()), BlocProvider(create: (context) => NotificationsCubit(notificationsStream: notificationsStreamController.stream)), - BlocProvider(create: (context) => createThunderBloc()), + BlocProvider(create: (context) => createAppBootstrapCubit()), + BlocProvider(create: (context) => createThunderCubit()), BlocProvider(create: (context) => GesturePreferencesCubit(preferencesStore: const UserPreferencesStore())), BlocProvider(create: (context) => FeedPreferencesCubit(preferencesStore: const UserPreferencesStore())), BlocProvider(create: (context) => CommentPreferencesCubit(preferences: const UserPreferencesStore())), BlocProvider(create: (context) => ThemePreferencesCubit(preferencesStore: const UserPreferencesStore())), BlocProvider(create: (context) => VideoPreferencesCubit(preferencesStore: const UserPreferencesStore())), BlocProvider(create: (context) => FabPreferencesCubit(preferencesStore: const UserPreferencesStore())), - BlocProvider(create: (context) => FabStateCubit()), - BlocProvider(create: (context) => NavBarStateCubit()), - BlocProvider(create: (context) => FeedUiCubit()), - BlocProvider(create: (context) => AnonymousSubscriptionsBloc()), + BlocProvider(create: (context) => ShellChromeCubit()), + BlocProvider(create: (context) => AnonymousSubscriptionsCubit()..loadSubscribedCommunities()), BlocProvider(create: (context) => createNetworkCheckerCubit()..getConnectionType()), ], child: BlocBuilder( builder: (context, state) { - final appLanguageCode = context.select((bloc) => bloc.state.appLanguageCode); + final appLanguageCode = context.select((bloc) => bloc.state.appLanguageCode); return DynamicColorBuilder( builder: (lightColorScheme, darkColorScheme) { @@ -142,39 +143,39 @@ class _ThunderAppState extends State { return OverlaySupport.global( child: AnnotatedRegion( value: FlexColorScheme.themedSystemNavigationBar(context, systemNavBarStyle: FlexSystemNavBarStyle.transparent), - child: BlocBuilder( - buildWhen: (previous, current) => previous.account.id != current.account.id, - builder: (context, profileState) { - final account = profileState.account; - return MultiBlocProvider( - key: ValueKey('account_${account.id}'), - providers: [ - BlocProvider(create: (context) => createInboxBloc(account)..add(GetInboxEvent(reset: true))), - BlocProvider(create: (context) => createSearchBloc(account)), - BlocProvider(create: (context) => createFeedBloc(account)), - ], - child: MaterialApp( - title: 'Thunder', - locale: locale, - localizationsDelegates: const [ - ...AppLocalizations.localizationsDelegates, - MaterialLocalizationsEo.delegate, - CupertinoLocalizationsEo.delegate, - ], - supportedLocales: const [ - ...AppLocalizations.supportedLocales, - Locale('eo'), - ], - themeMode: state.themeType == ThemeType.system ? ThemeMode.system : (state.themeType == ThemeType.light ? ThemeMode.light : ThemeMode.dark), - theme: theme, - darkTheme: darkTheme, - debugShowCheckedModeBanner: false, - scaffoldMessengerKey: GlobalContext.scaffoldMessengerKey, - scrollBehavior: (state.reduceAnimations && Platform.isAndroid) ? const ScrollBehavior().copyWith(overscroll: false) : null, - home: Thunder(pageController: thunderPageController), - ), - ); - }, + child: MaterialApp( + title: 'Thunder', + locale: locale, + localizationsDelegates: const [ + ...AppLocalizations.localizationsDelegates, + MaterialLocalizationsEo.delegate, + CupertinoLocalizationsEo.delegate, + ], + supportedLocales: const [ + ...AppLocalizations.supportedLocales, + Locale('eo'), + ], + themeMode: state.themeType == ThemeType.system ? ThemeMode.system : (state.themeType == ThemeType.light ? ThemeMode.light : ThemeMode.dark), + theme: theme, + darkTheme: darkTheme, + debugShowCheckedModeBanner: false, + scaffoldMessengerKey: GlobalContext.scaffoldMessengerKey, + scrollBehavior: (state.reduceAnimations && Platform.isAndroid) ? const ScrollBehavior().copyWith(overscroll: false) : null, + home: Session( + builder: (context, sessionState) { + final account = sessionState.activeAccount!; + + return SessionScope( + account: account, + generation: sessionState.generation, + profileBloc: createProfileBloc, + inboxBloc: createInboxBloc, + searchBloc: createSearchBloc, + feedBloc: createFeedBloc, + builder: (context, profileState) => Thunder(pageController: thunderPageController), + ); + }, + ), ), ), ); diff --git a/lib/src/app/shell/widgets/bottom_nav_bar.dart b/lib/src/app/shell/widgets/bottom_nav_bar.dart index a884ce8c6..4611f86d4 100644 --- a/lib/src/app/shell/widgets/bottom_nav_bar.dart +++ b/lib/src/app/shell/widgets/bottom_nav_bar.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; +import 'package:thunder/src/app/shell/state/shell_chrome_cubit.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/inbox/inbox.dart'; @@ -13,7 +14,7 @@ import 'package:thunder/src/features/settings/api.dart'; /// A custom bottom navigation bar that enables tap/swipe gestures class CustomBottomNavigationBar extends StatefulWidget { - const CustomBottomNavigationBar({super.key, required this.selectedPageIndex, required this.onPageChange}); + const CustomBottomNavigationBar({super.key, required this.selectedPageIndex, required this.onPageChange, this.feedActionController}); /// The index of the currently selected page final int selectedPageIndex; @@ -21,6 +22,9 @@ class CustomBottomNavigationBar extends StatefulWidget { /// Callback function that is triggered when a page is changed final Function(int index) onPageChange; + /// Optional controller for the root feed page. + final FeedActionController? feedActionController; + @override State createState() => _CustomBottomNavigationBarState(); } @@ -79,7 +83,7 @@ class _CustomBottomNavigationBarState extends State { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final showNavigationLabels = context.select((bloc) => bloc.state.showNavigationLabels); + final showNavigationLabels = context.select((bloc) => bloc.state.showNavigationLabels); final bottomNavBarDoubleTapGestures = context.select((cubit) => cubit.state.bottomNavBarDoubleTapGestures); final inboxState = context.watch().state; @@ -134,12 +138,12 @@ class _CustomBottomNavigationBarState extends State { ), ], onDestinationSelected: (index) { - if (context.read().state.isFeedFabOpen) { - context.read().setFeedFabOpen(false); + if (context.read().state.isFeedFabOpen) { + context.read().setFeedFabOpen(false); } if (widget.selectedPageIndex == 0 && index == 0) { - context.read().scrollToTop(); + widget.feedActionController?.scrollToTop(); } if (widget.selectedPageIndex == 1 && index != 1) { diff --git a/lib/src/app/shell/widgets/session.dart b/lib/src/app/shell/widgets/session.dart new file mode 100644 index 000000000..9bb21bb79 --- /dev/null +++ b/lib/src/app/shell/widgets/session.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thunder/l10n/generated/app_localizations.dart'; + +import 'package:thunder/src/features/session/api.dart'; + +class Session extends StatelessWidget { + const Session({super.key, required this.builder}); + + final Widget Function(BuildContext context, SessionState sessionState) builder; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return BlocBuilder( + builder: (context, sessionState) { + switch (sessionState.status) { + case SessionStatus.initial: + case SessionStatus.loading: + return const _SessionStatusView(); + case SessionStatus.failure: + return _SessionStatusView( + message: sessionState.error, + actionLabel: l10n.retry, + onAction: () => context.read().add(const SessionInitialized()), + ); + case SessionStatus.success: + final account = sessionState.activeAccount; + if (account == null) { + return _SessionStatusView( + message: l10n.unexpectedError, + actionLabel: l10n.retry, + onAction: () => context.read().add(const SessionInitialized()), + ); + } + + return builder(context, sessionState); + } + }, + ); + } +} + +class _SessionStatusView extends StatelessWidget { + const _SessionStatusView({this.message, this.actionLabel, this.onAction}); + + final String? message; + final String? actionLabel; + final VoidCallback? onAction; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Scaffold( + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text(message ?? l10n.loading, textAlign: TextAlign.center), + if (actionLabel != null && onAction != null) ...[ + const SizedBox(height: 16), + FilledButton(onPressed: onAction, child: Text(actionLabel!)), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/app/shell/widgets/session_scope.dart b/lib/src/app/shell/widgets/session_scope.dart new file mode 100644 index 000000000..c8997b151 --- /dev/null +++ b/lib/src/app/shell/widgets/session_scope.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:thunder/src/features/account/api.dart'; +import 'package:thunder/src/features/feed/api.dart'; +import 'package:thunder/src/features/inbox/api.dart'; +import 'package:thunder/src/features/search/api.dart'; + +class SessionScope extends StatelessWidget { + const SessionScope({ + super.key, + required this.account, + required this.generation, + required this.profileBloc, + required this.inboxBloc, + required this.searchBloc, + required this.feedBloc, + required this.builder, + }); + + /// The [account] for which the session scope is created. + final Account account; + + /// The [generation] is used to force the recreation of the session scope when the account changes. + final int generation; + + /// The [profileBloc] is used to create the [ProfileBloc] for the session scope. + final ProfileBloc Function(Account account) profileBloc; + + /// The [inboxBloc] is used to create the [InboxBloc] for the session scope. + final InboxBloc Function(Account account) inboxBloc; + + /// The [searchBloc] is used to create the [SearchBloc] for the session scope. + final SearchBloc Function(Account account) searchBloc; + + /// The [feedBloc] is used to create the [FeedBloc] for the session scope. + final FeedBloc Function(Account account) feedBloc; + + /// The [builder] is used to build the UI for the session scope. + final Widget Function(BuildContext context, ProfileState profileState) builder; + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + key: ValueKey('session_scope_${account.id}_$generation'), + providers: [ + BlocProvider(create: (context) => profileBloc(account)..add(InitializeAuth())), + BlocProvider(create: (context) => inboxBloc(account)..add(GetInboxEvent(reset: true))), + BlocProvider(create: (context) => searchBloc(account)), + BlocProvider(create: (context) => feedBloc(account)), + ], + child: BlocBuilder( + buildWhen: (previous, current) => previous.account.id != current.account.id, + builder: builder, + ), + ); + } +} diff --git a/lib/src/app/state/app_bootstrap_cubit/app_bootstrap_cubit.dart b/lib/src/app/state/app_bootstrap_cubit/app_bootstrap_cubit.dart new file mode 100644 index 000000000..80f47c274 --- /dev/null +++ b/lib/src/app/state/app_bootstrap_cubit/app_bootstrap_cubit.dart @@ -0,0 +1,44 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +import 'package:thunder/src/foundation/contracts/contracts.dart'; +import 'package:thunder/src/foundation/errors/errors.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; + +part 'app_bootstrap_state.dart'; + +class AppBootstrapCubit extends Cubit { + AppBootstrapCubit({ + required VersionChecker versionChecker, + }) : _versionChecker = versionChecker, + super(const AppBootstrapState()); + + final VersionChecker _versionChecker; + + Future initialize() async { + emit(state.copyWith( + status: AppBootstrapStatus.loading, + errorMessage: null, + errorReason: null, + )); + + try { + final version = await _versionChecker.fetchLatestVersion(); + + emit(state.copyWith( + status: AppBootstrapStatus.success, + version: version, + errorMessage: null, + errorReason: null, + )); + } catch (e) { + final message = e.toString(); + + emit(state.copyWith( + status: AppBootstrapStatus.failure, + errorMessage: message, + errorReason: AppErrorReason.unexpected(message: message, details: message), + )); + } + } +} diff --git a/lib/src/app/state/app_bootstrap_cubit/app_bootstrap_state.dart b/lib/src/app/state/app_bootstrap_cubit/app_bootstrap_state.dart new file mode 100644 index 000000000..cdcf01b82 --- /dev/null +++ b/lib/src/app/state/app_bootstrap_cubit/app_bootstrap_state.dart @@ -0,0 +1,36 @@ +part of 'app_bootstrap_cubit.dart'; + +enum AppBootstrapStatus { initial, loading, success, failure } + +const _appBootstrapStateUnset = Object(); + +class AppBootstrapState extends Equatable { + const AppBootstrapState({ + this.status = AppBootstrapStatus.initial, + this.version, + this.errorMessage, + this.errorReason, + }); + + final AppBootstrapStatus status; + final Version? version; + final String? errorMessage; + final AppErrorReason? errorReason; + + AppBootstrapState copyWith({ + AppBootstrapStatus? status, + Object? version = _appBootstrapStateUnset, + Object? errorMessage = _appBootstrapStateUnset, + Object? errorReason = _appBootstrapStateUnset, + }) { + return AppBootstrapState( + status: status ?? this.status, + version: identical(version, _appBootstrapStateUnset) ? this.version : version as Version?, + errorMessage: identical(errorMessage, _appBootstrapStateUnset) ? this.errorMessage : errorMessage as String?, + errorReason: identical(errorReason, _appBootstrapStateUnset) ? this.errorReason : errorReason as AppErrorReason?, + ); + } + + @override + List get props => [status, version, errorMessage, errorReason]; +} diff --git a/lib/src/app/state/thunder/thunder_bloc.dart b/lib/src/app/state/thunder/thunder_bloc.dart index ba418d76a..4b84650c2 100644 --- a/lib/src/app/state/thunder/thunder_bloc.dart +++ b/lib/src/app/state/thunder/thunder_bloc.dart @@ -1,65 +1,22 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:bloc_concurrency/bloc_concurrency.dart'; -import 'package:stream_transform/stream_transform.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/notification/notification.dart'; import 'package:thunder/src/foundation/contracts/contracts.dart'; -import 'package:thunder/src/foundation/errors/errors.dart'; -import 'package:thunder/src/foundation/config/config.dart'; -part 'thunder_event.dart'; part 'thunder_state.dart'; -const throttleDuration = Duration(milliseconds: 300); - -EventTransformer throttleDroppable(Duration duration) { - return (events, mapper) => droppable().call(events.throttle(duration), mapper); -} - -class ThunderBloc extends Bloc { - ThunderBloc({ +class ThunderCubit extends Cubit { + ThunderCubit({ required PreferencesStore preferencesStore, - required VersionChecker versionChecker, }) : _preferencesStore = preferencesStore, - _versionChecker = versionChecker, - super(const ThunderState()) { - on(_initializeAppEvent, transformer: throttleDroppable(throttleDuration)); - on(_userPreferencesChangeEvent, transformer: throttleDroppable(throttleDuration)); - on(_onSetCurrentAnonymousInstance); - } + super(const ThunderState()); final PreferencesStore _preferencesStore; - final VersionChecker _versionChecker; - /// This event should be triggered at the start of the app. - /// - /// It initializes the local database, checks for updates from GitHub, and loads the user's preferences. - Future _initializeAppEvent(InitializeAppEvent event, Emitter emit) async { + Future reload() async { try { - // Check for any updates from GitHub - final version = await _versionChecker.fetchLatestVersion(); - - add(UserPreferencesChangeEvent()); - emit(state.copyWith(status: ThunderStatus.success, version: version)); - } catch (e) { - final message = e.toString(); - - return emit( - state.copyWith( - status: ThunderStatus.failure, - errorMessage: message, - errorReason: AppErrorReason.unexpected(message: message, details: message), - ), - ); - } - } - - Future _userPreferencesChangeEvent(UserPreferencesChangeEvent event, Emitter emit) async { - try { - emit(state.copyWith(status: ThunderStatus.refreshing)); - // Tablet Settings bool tabletMode = _preferencesStore.getLocalSetting(LocalSettings.useTabletMode) ?? false; @@ -72,15 +29,13 @@ class ThunderBloc extends Bloc { String? appLanguageCode = _preferencesStore.getLocalSetting(LocalSettings.appLanguageCode) ?? 'en'; bool useProfilePictureForDrawer = _preferencesStore.getLocalSetting(LocalSettings.useProfilePictureForDrawer) ?? false; ImageCachingMode imageCachingMode = ImageCachingMode.values.byName(_preferencesStore.getLocalSetting(LocalSettings.imageCachingMode) ?? ImageCachingMode.relaxed.name); + bool enableExperimentalFeatures = _preferencesStore.getLocalSetting(LocalSettings.enableExperimentalFeatures) ?? false; bool showNavigationLabels = _preferencesStore.getLocalSetting(LocalSettings.showNavigationLabels) ?? true; bool hideTopBarOnScroll = _preferencesStore.getLocalSetting(LocalSettings.hideTopBarOnScroll) ?? false; bool hideBottomBarOnScroll = _preferencesStore.getLocalSetting(LocalSettings.hideBottomBarOnScroll) ?? false; bool scoreCounters = _preferencesStore.getLocalSetting(LocalSettings.scoreCounters) ?? false; - String currentAnonymousInstance = _preferencesStore.getLocalSetting(LocalSettings.currentAnonymousInstance) ?? DEFAULT_INSTANCE; - - return emit(state.copyWith( - status: ThunderStatus.success, + emit(state.copyWith( tabletMode: tabletMode, browserMode: browserMode, openInReaderMode: openInReaderMode, @@ -90,33 +45,14 @@ class ThunderBloc extends Bloc { appLanguageCode: appLanguageCode, useProfilePictureForDrawer: useProfilePictureForDrawer, imageCachingMode: imageCachingMode, + enableExperimentalFeatures: enableExperimentalFeatures, showNavigationLabels: showNavigationLabels, hideTopBarOnScroll: hideTopBarOnScroll, hideBottomBarOnScroll: hideBottomBarOnScroll, scoreCounters: scoreCounters, - currentAnonymousInstance: currentAnonymousInstance, - errorReason: null, - )); - } catch (e) { - final message = e.toString(); - return emit(state.copyWith( - status: ThunderStatus.failure, - errorMessage: message, - errorReason: AppErrorReason.unexpected( - message: message, - details: e.toString(), - ), )); + } catch (_) { + rethrow; } } - - Future _onSetCurrentAnonymousInstance(OnSetCurrentAnonymousInstance event, Emitter emit) async { - if (event.instance != null) { - _preferencesStore.setSetting(LocalSettings.currentAnonymousInstance, event.instance!); - } else { - _preferencesStore.removeSetting(LocalSettings.currentAnonymousInstance); - } - - return emit(state.copyWith(currentAnonymousInstance: event.instance)); - } } diff --git a/lib/src/app/state/thunder/thunder_event.dart b/lib/src/app/state/thunder/thunder_event.dart deleted file mode 100644 index 2a0e3cf04..000000000 --- a/lib/src/app/state/thunder/thunder_event.dart +++ /dev/null @@ -1,30 +0,0 @@ -part of 'thunder_bloc.dart'; - -abstract class ThunderEvent extends Equatable { - const ThunderEvent(); - - @override - List get props => []; -} - -class UserPreferencesChangeEvent extends ThunderEvent {} - -class InitializeAppEvent extends ThunderEvent {} - -class OnScrollToTopEvent extends ThunderEvent {} - -class OnDismissEvent extends ThunderEvent { - final bool isBeingDismissed; - const OnDismissEvent(this.isBeingDismissed); - - @override - List get props => [isBeingDismissed]; -} - -class OnSetCurrentAnonymousInstance extends ThunderEvent { - final String? instance; - const OnSetCurrentAnonymousInstance(this.instance); - - @override - List get props => [instance]; -} diff --git a/lib/src/app/state/thunder/thunder_state.dart b/lib/src/app/state/thunder/thunder_state.dart index 299d5d816..c74b5ff63 100644 --- a/lib/src/app/state/thunder/thunder_state.dart +++ b/lib/src/app/state/thunder/thunder_state.dart @@ -1,18 +1,9 @@ part of 'thunder_bloc.dart'; -enum ThunderStatus { initial, loading, refreshing, success, failure } - const _thunderStateUnset = Object(); class ThunderState extends Equatable { const ThunderState({ - this.status = ThunderStatus.initial, - - // General - this.version, - this.errorMessage, - this.errorReason, - // Tablet Settings this.tabletMode = false, @@ -25,18 +16,13 @@ class ThunderState extends Equatable { this.inboxNotificationType = NotificationType.none, this.scoreCounters = false, this.imageCachingMode = ImageCachingMode.relaxed, + this.enableExperimentalFeatures = false, this.showNavigationLabels = true, this.hideTopBarOnScroll = false, this.hideBottomBarOnScroll = false, this.appLanguageCode = 'en', - this.currentAnonymousInstance = DEFAULT_INSTANCE, }); - final ThunderStatus status; - final Version? version; - final String? errorMessage; - final AppErrorReason? errorReason; - // Tablet Settings final bool tabletMode; @@ -49,17 +35,13 @@ class ThunderState extends Equatable { final NotificationType inboxNotificationType; final bool scoreCounters; final ImageCachingMode imageCachingMode; + final bool enableExperimentalFeatures; final bool showNavigationLabels; final bool hideTopBarOnScroll; final bool hideBottomBarOnScroll; final String? appLanguageCode; - final String? currentAnonymousInstance; ThunderState copyWith({ - ThunderStatus? status, - Object? version = _thunderStateUnset, - Object? errorMessage = _thunderStateUnset, - Object? errorReason = _thunderStateUnset, bool? tabletMode, BrowserMode? browserMode, bool? openInReaderMode, @@ -69,17 +51,13 @@ class ThunderState extends Equatable { NotificationType? inboxNotificationType, bool? scoreCounters, ImageCachingMode? imageCachingMode, + bool? enableExperimentalFeatures, bool? showNavigationLabels, bool? hideTopBarOnScroll, bool? hideBottomBarOnScroll, Object? appLanguageCode = _thunderStateUnset, - Object? currentAnonymousInstance = _thunderStateUnset, }) { return ThunderState( - status: status ?? this.status, - version: identical(version, _thunderStateUnset) ? this.version : version as Version?, - errorMessage: identical(errorMessage, _thunderStateUnset) ? this.errorMessage : errorMessage as String?, - errorReason: identical(errorReason, _thunderStateUnset) ? this.errorReason : errorReason as AppErrorReason?, tabletMode: tabletMode ?? this.tabletMode, browserMode: browserMode ?? this.browserMode, openInReaderMode: openInReaderMode ?? this.openInReaderMode, @@ -89,20 +67,16 @@ class ThunderState extends Equatable { inboxNotificationType: inboxNotificationType ?? this.inboxNotificationType, scoreCounters: scoreCounters ?? this.scoreCounters, imageCachingMode: imageCachingMode ?? this.imageCachingMode, + enableExperimentalFeatures: enableExperimentalFeatures ?? this.enableExperimentalFeatures, showNavigationLabels: showNavigationLabels ?? this.showNavigationLabels, hideTopBarOnScroll: hideTopBarOnScroll ?? this.hideTopBarOnScroll, hideBottomBarOnScroll: hideBottomBarOnScroll ?? this.hideBottomBarOnScroll, appLanguageCode: identical(appLanguageCode, _thunderStateUnset) ? this.appLanguageCode : appLanguageCode as String?, - currentAnonymousInstance: identical(currentAnonymousInstance, _thunderStateUnset) ? this.currentAnonymousInstance : currentAnonymousInstance as String?, ); } @override List get props => [ - status, - version, - errorMessage, - errorReason, tabletMode, browserMode, openInReaderMode, @@ -112,10 +86,10 @@ class ThunderState extends Equatable { inboxNotificationType, scoreCounters, imageCachingMode, + enableExperimentalFeatures, showNavigationLabels, hideTopBarOnScroll, hideBottomBarOnScroll, appLanguageCode, - currentAnonymousInstance, ]; } diff --git a/lib/src/app/wiring/fetch_active_account_provider.dart b/lib/src/app/wiring/fetch_active_account_provider.dart deleted file mode 100644 index 4682f419a..000000000 --- a/lib/src/app/wiring/fetch_active_account_provider.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:thunder/src/foundation/contracts/active_account_provider.dart'; -import 'package:thunder/src/features/account/account.dart'; - -class FetchActiveAccountProvider implements ActiveAccountProvider { - const FetchActiveAccountProvider(); - - @override - Future getActiveAccount() { - return fetchActiveProfile(); - } -} diff --git a/lib/src/app/wiring/state_factories.dart b/lib/src/app/wiring/state_factories.dart index c71653f46..9bbad190a 100644 --- a/lib/src/app/wiring/state_factories.dart +++ b/lib/src/app/wiring/state_factories.dart @@ -1,5 +1,5 @@ +import 'package:thunder/src/app/state/app_bootstrap_cubit/app_bootstrap_cubit.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; -import 'package:thunder/src/app/wiring/fetch_active_account_provider.dart'; import 'package:thunder/src/app/wiring/nodeinfo_platform_detection_service.dart'; import 'package:thunder/src/app/state/deep_links_cubit/deep_links_cubit.dart'; import 'package:thunder/src/app/state/network_checker_cubit/network_checker_cubit.dart'; @@ -15,16 +15,22 @@ import 'package:thunder/src/features/moderator/api.dart'; import 'package:thunder/src/features/notification/api.dart'; import 'package:thunder/src/features/post/api.dart'; import 'package:thunder/src/features/search/api.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/features/user/api.dart'; import 'package:thunder/src/features/instance/presentation/state/instance_page_bloc.dart'; -ThunderBloc createThunderBloc() { - return ThunderBloc( - preferencesStore: const UserPreferencesStore(), +AppBootstrapCubit createAppBootstrapCubit() { + return AppBootstrapCubit( versionChecker: const GithubVersionChecker(), ); } +ThunderCubit createThunderCubit() { + return ThunderCubit( + preferencesStore: const UserPreferencesStore(), + ); +} + DeepLinksCubit createDeepLinksCubit() { return DeepLinksCubit( deepLinkService: AppLinksDeepLinkService(), @@ -45,9 +51,17 @@ ProfileBloc createProfileBloc(Account account) { accountRepositoryFactory: (account) => AccountRepositoryImpl(account: account), userRepositoryFactory: (account) => UserRepositoryImpl(account: account), platformDetectionService: const NodeInfoPlatformDetectionService(), - activeAccountProvider: const FetchActiveAccountProvider(), localizationService: const GlobalContextLocalizationService(), - preferencesStore: const UserPreferencesStore(), + ); +} + +SessionBloc createSessionBloc() { + return SessionBloc( + sessionRepository: const PersistentSessionRepository(), + accountRepositoryFactory: (account) => AccountRepositoryImpl(account: account), + instanceRepositoryFactory: (account) => InstanceRepositoryImpl(account: account), + platformDetectionService: const NodeInfoPlatformDetectionService(), + localizationService: const GlobalContextLocalizationService(), ); } @@ -144,21 +158,37 @@ CreateCommentCubit createCreateCommentCubit(Account account) { ); } -UserSettingsBloc createUserSettingsBloc(Account account) { - return UserSettingsBloc( +AccountSettingsCubit createAccountSettingsCubit(Account account, {ThunderSiteResponse? initialSiteResponse}) { + return AccountSettingsCubit( + account: account, + accountRepository: AccountRepositoryImpl(account: account), + localizationService: const GlobalContextLocalizationService(), + initialSiteResponse: initialSiteResponse, + ); +} + +UserBlocksCubit createUserBlocksCubit(Account account) { + return UserBlocksCubit( account: account, instanceRepository: InstanceRepositoryImpl(account: account), - searchRepository: SearchRepositoryImpl(account: account), communityRepository: CommunityRepositoryImpl(account: account), - accountRepository: AccountRepositoryImpl(account: account), userRepository: UserRepositoryImpl(account: account), - activeAccountProvider: const FetchActiveAccountProvider(), localizationService: const GlobalContextLocalizationService(), ); } -ReportBloc createReportBloc() { +UserMediaCubit createUserMediaCubit(Account account) { + return UserMediaCubit( + account: account, + accountRepository: AccountRepositoryImpl(account: account), + searchRepository: SearchRepositoryImpl(account: account), + localizationService: const GlobalContextLocalizationService(), + ); +} + +ReportBloc createReportBloc(Account account) { return ReportBloc( + account: account, localizationService: const GlobalContextLocalizationService(), ); } diff --git a/lib/src/features/account/data/cache/profile_site_info_cache.dart b/lib/src/features/account/data/cache/profile_site_info_cache.dart index 53fd68b61..0172b9d24 100644 --- a/lib/src/features/account/data/cache/profile_site_info_cache.dart +++ b/lib/src/features/account/data/cache/profile_site_info_cache.dart @@ -15,7 +15,7 @@ class ProfileSiteInfoCache { final _cacheByAccountKey = HashMap(); /// Default time-to-live for cache entries - final defaultTTL = const Duration(seconds: 30); + final defaultTTL = const Duration(seconds: 300); /// Returns a unique key for the given [account] String _accountKey(Account account) => '${account.instance}:${account.anonymous}:${account.id}'; diff --git a/lib/src/features/account/presentation/pages/login_page.dart b/lib/src/features/account/presentation/pages/login_page.dart index 52c38537b..c6d31c625 100644 --- a/lib/src/features/account/presentation/pages/login_page.dart +++ b/lib/src/features/account/presentation/pages/login_page.dart @@ -10,8 +10,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/instance/data/constants/known_instances.dart'; +import 'package:thunder/src/features/session/api.dart'; -import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/src/app/shell/navigation/link_navigation_utils.dart'; @@ -120,20 +120,22 @@ class _LoginPageState extends State with SingleTickerProviderStateMix return MultiBlocListener( listeners: [ - BlocListener( - listenWhen: (previous, current) { - if (previous.status == ProfileStatus.initial && current.status == ProfileStatus.success) { - widget.popModal(); - showSnackbar(l10n.loginSucceeded); - } - return true; - }, + BlocListener( + listenWhen: (previous, current) => previous.mutationStatus != current.mutationStatus, listener: (listenerContext, state) async { - if (state.status == ProfileStatus.loading) { + if (state.lastMutation != SessionMutationType.authenticatedLogin) { + return; + } + + if (state.mutationStatus == SessionMutationStatus.loading) { setState(() => isLoading = true); - } else if (state.status == ProfileStatus.failure) { + } else if (state.mutationStatus == SessionMutationStatus.failure) { setState(() => isLoading = false); showSnackbar(l10n.loginFailed(state.error ?? l10n.missingErrorMessage)); + } else if (state.mutationStatus == SessionMutationStatus.success) { + setState(() => isLoading = false); + widget.popModal(); + showSnackbar(l10n.loginSucceeded); } }, ), @@ -450,30 +452,27 @@ class _LoginPageState extends State with SingleTickerProviderStateMix return; } - await Account.insertAnonymousInstance(Account( - id: '', - instance: _instanceTextEditingController.text, - index: -1, - anonymous: true, - platform: instanceInfo?.platform, - )); - - context.read().add(OnSetCurrentAnonymousInstance(_instanceTextEditingController.text)); - context.read().add(SwitchProfile(accountId: _instanceTextEditingController.text)); + context.read().add(AnonymousSessionAdded( + account: Account( + id: '', + instance: _instanceTextEditingController.text, + index: -1, + anonymous: true, + platform: instanceInfo?.platform, + ), + activate: true, + )); widget.popRegister(); return; } // Perform login authentication - context.read().add( - AddProfile( - username: _usernameTextEditingController.text, - password: _passwordTextEditingController.text, - instance: _instanceTextEditingController.text.trim(), - totp: _totpTextEditingController.text, - showContentWarning: false, - ), - ); + context.read().add(AuthenticatedLoginRequested( + username: _usernameTextEditingController.text, + password: _passwordTextEditingController.text, + instance: _instanceTextEditingController.text.trim(), + totp: _totpTextEditingController.text, + )); } } diff --git a/lib/src/features/account/presentation/state/profile_bloc.dart b/lib/src/features/account/presentation/state/profile_bloc.dart index d48cc3bf7..763db6530 100644 --- a/lib/src/features/account/presentation/state/profile_bloc.dart +++ b/lib/src/features/account/presentation/state/profile_bloc.dart @@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart'; import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; -import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -29,7 +28,7 @@ EventTransformer throttleDroppable(Duration duration) { } class ProfileBloc extends Bloc { - Account account; + final Account account; InstanceRepository? instanceRepository; AccountRepository? accountRepository; @@ -39,9 +38,7 @@ class ProfileBloc extends Bloc { final AccountRepository Function(Account) _accountRepositoryFactory; final UserRepository Function(Account) _userRepositoryFactory; final PlatformDetectionService _platformDetectionService; - final ActiveAccountProvider _activeAccountProvider; final LocalizationService _localizationService; - final PreferencesStore _preferencesStore; ProfileBloc({ required this.account, @@ -49,31 +46,16 @@ class ProfileBloc extends Bloc { required AccountRepository Function(Account) accountRepositoryFactory, required UserRepository Function(Account) userRepositoryFactory, required PlatformDetectionService platformDetectionService, - required ActiveAccountProvider activeAccountProvider, required LocalizationService localizationService, - required PreferencesStore preferencesStore, }) : _instanceRepositoryFactory = instanceRepositoryFactory, _accountRepositoryFactory = accountRepositoryFactory, _userRepositoryFactory = userRepositoryFactory, _platformDetectionService = platformDetectionService, - _activeAccountProvider = activeAccountProvider, _localizationService = localizationService, - _preferencesStore = preferencesStore, super(ProfileState(account: account)) { // This event should be triggered during the start of the app, or when there is a change in the active account on(_initializeAuth, transformer: restartable()); - /// This event should be triggered whenever the user removes a profile - /// This could be either a log out event, or a removal of a profile - on(_removeProfile); - - /// This event occurs whenever you switch to a different profile - on(_switchProfile); - - /// This event should be triggered whenever the user adds a profile. - /// This could be addition of a anonymous or non-anonymous account. - on(_addProfile); - /// This event handles fetching a given profile's information. /// For non-anonymous accounts, this includes user information, subscriptions, and favourites. /// For anonymous accounts, this will not do anything. @@ -149,141 +131,6 @@ class ProfileBloc extends Bloc { return; } - Future _addProfile(AddProfile event, Emitter emit) async { - try { - emit(state.copyWith( - status: ProfileStatus.loading, - error: () => null, - errorReason: () => null, - )); - - final instanceUrl = event.instance.replaceAll('https://', ''); - - // Detect the platform before attempting to log in - final platformInfo = await _platformDetectionService.detectPlatform(instanceUrl) ?? {'platform': ThreadiversePlatform.lemmy}; - final platform = platformInfo['platform']; - - // Create a temporary Account to attempt to log in - Account tempAccount = Account(id: '', index: -1, instance: instanceUrl, platform: platform); - - // Create a temporary account repository to use for the login - final jwt = await _accountRepositoryFactory(tempAccount).login(username: event.username, password: event.password, totp: event.totp); - if (jwt == null) { - final message = _localizationService.l10n.unexpectedError; - return emit(state.copyWith( - status: ProfileStatus.failure, - error: () => message, - errorReason: () => AppErrorReason.actionFailed(message: message), - )); - } - - // Create a temporary instance repository to use for the site information - tempAccount = Account(id: '', index: -1, jwt: jwt, instance: tempAccount.instance, platform: platform); - final siteResponse = await _instanceRepositoryFactory(tempAccount).info(); - - if (event.showContentWarning && siteResponse.site.contentWarning?.isNotEmpty == true) { - return emit(state.copyWith(status: ProfileStatus.contentWarning, contentWarning: () => siteResponse.site.contentWarning!)); - } - - // Create a new account in the database - Account? account = Account( - id: '', - username: siteResponse.myUser?.localUserView.person.name, - jwt: jwt, - instance: tempAccount.instance, - userId: siteResponse.myUser?.localUserView.person.id, - index: -1, - platform: platform, - ); - - account = await Account.insertAccount(account); - if (account == null) { - final message = _localizationService.l10n.unexpectedError; - return emit(state.copyWith( - status: ProfileStatus.failure, - error: () => message, - errorReason: () => AppErrorReason.unexpected(message: message), - )); - } - - // Set this account as the active account - this.account = account; - await _preferencesStore.setString('active_profile_id', account.id); - - // Run the CheckAuth event to reset everything - return await _initializeAuth(InitializeAuth(), emit); - } catch (e) { - debugPrint('Error adding profile: ${e.toString()}'); - final message = getExceptionErrorMessage(e); - return emit(state.copyWith( - status: ProfileStatus.failure, - error: () => message, - errorReason: () => AppErrorReason.unexpected(message: message), - )); - } - } - - Future _switchProfile(SwitchProfile event, Emitter emit) async { - emit(state.copyWith( - status: ProfileStatus.loading, - reload: event.reload, - error: () => null, - errorReason: () => null, - )); - - Account? account = await Account.fetchAccount(event.accountId); - - if (account != null) { - // Set this account as the active account - await _preferencesStore.setString('active_profile_id', event.accountId); - } else { - // Account was not found - this indicates is an anonymous account. Find the corresponding account - final anonymousAccounts = await Account.anonymousInstances(); - final anonymousAccount = anonymousAccounts.firstWhereOrNull((element) => element.instance == event.accountId); - account = anonymousAccount; - - await _preferencesStore.remove('active_profile_id'); - } - - if (account == null) { - final message = _localizationService.l10n.unexpectedError; - return emit(state.copyWith( - status: ProfileStatus.failure, - error: () => message, - errorReason: () => AppErrorReason.unexpected(message: message), - )); - } - - this.account = account; - add(InitializeAuth()); - } - - Future _removeProfile(RemoveProfile event, Emitter emit) async { - emit(state.copyWith( - status: ProfileStatus.loading, - error: () => null, - errorReason: () => null, - )); - - final account = await _activeAccountProvider.getActiveAccount(); - await Account.deleteAccount(event.accountId); - - if (!account.anonymous && account.id == event.accountId) { - // The removed profile is the currently active profile. Remove this. - await _preferencesStore.remove('active_profile_id'); - add(InitializeAuth()); - } else if (account.anonymous && account.instance == event.accountId) { - // The removed profile is the current anonymous profile. - add(InitializeAuth()); - } - - return emit(state.copyWith( - status: ProfileStatus.success, - error: () => null, - errorReason: () => null, - )); - } - Future _cancelLoginAttempt(CancelLoginAttempt event, Emitter emit) async { final message = _localizationService.l10n.loginAttemptCanceled; return emit(state.copyWith( @@ -296,7 +143,6 @@ class ProfileBloc extends Bloc { /// Fetches the current profile's information, including the user's information and moderated communities. /// This is only applicable for non-anonymous profiles. Future _fetchProfileInformation(FetchProfileInformation event, Emitter emit) async { - final account = await _activeAccountProvider.getActiveAccount(); if (account.anonymous) { return emit(state.copyWith( status: ProfileStatus.success, @@ -359,7 +205,6 @@ class ProfileBloc extends Bloc { /// Fetches the current profile's account settings. This is only applicable for non-anonymous profiles. Future _fetchProfileSettings(FetchProfileSettings event, Emitter emit) async { - final account = await _activeAccountProvider.getActiveAccount(); if (account.anonymous) { return emit(state.copyWith( status: ProfileStatus.success, @@ -398,7 +243,6 @@ class ProfileBloc extends Bloc { /// Fetches the current profile's subscribed communities. This is only applicable for non-anonymous profiles. Future _fetchProfileSubscriptions(FetchProfileSubscriptions event, Emitter emit) async { - final account = await _activeAccountProvider.getActiveAccount(); if (account.anonymous) { return emit(state.copyWith( status: ProfileStatus.success, @@ -442,7 +286,6 @@ class ProfileBloc extends Bloc { /// Fetches the current profile's favourited communities. This is only applicable for non-anonymous profiles. Future _fetchProfileFavorites(FetchProfileFavorites event, Emitter emit) async { - final account = await _activeAccountProvider.getActiveAccount(); if (account.anonymous) { return emit(state.copyWith( status: ProfileStatus.success, diff --git a/lib/src/features/account/presentation/state/profile_event.dart b/lib/src/features/account/presentation/state/profile_event.dart index 81a6aa843..985ce7602 100644 --- a/lib/src/features/account/presentation/state/profile_event.dart +++ b/lib/src/features/account/presentation/state/profile_event.dart @@ -14,68 +14,15 @@ abstract class ProfileEvent extends Equatable { /// This is responsible for checking the authentication status of the user on app initialization. class InitializeAuth extends ProfileEvent {} -/// The [AddProfile] event should be triggered whenever the user attempts to log in for the first time. -/// This event is responsible for login authentication and handling related errors. -class AddProfile extends ProfileEvent { - final String username; - final String password; - final String instance; - final String totp; - final bool showContentWarning; - - const AddProfile({required this.username, required this.password, required this.instance, this.totp = "", this.showContentWarning = true}); - - @override - List get props => [reload, username, password, instance, totp, showContentWarning]; -} - /// Cancels a login attempt by emitting the `failure` state. class CancelLoginAttempt extends ProfileEvent { const CancelLoginAttempt(); } -/// TODO: Consolidate logic to have adding accounts (for both authenticated and anonymous accounts) placed here -class AddAccount extends ProfileEvent {} - -/// The [RemoveProfile] event should be triggered whenever the user removes a given account. -/// Currently, this event only handles removing authenticated accounts. -/// -/// TODO: Consolidate logic so that anonymous accounts are also handled here. -class RemoveProfile extends ProfileEvent { - final String accountId; - - const RemoveProfile({required this.accountId}); - - @override - List get props => [reload, accountId]; -} - -/// TODO: Consolidate logic to have removing accounts (for both authenticated and anonymous accounts) placed here -class RemoveAllAccounts extends ProfileEvent { - const RemoveAllAccounts(); -} - -/// The [SwitchProfile] event should be triggered whenever the user switches accounts. -/// Currently, this event only handles switching between authenticated accounts. -/// -/// TODO: Consolidate logic so that anonymous accounts are also handled here. -class SwitchProfile extends ProfileEvent { - final String accountId; - - const SwitchProfile({required this.accountId, super.reload = true}); - - @override - List get props => [reload, accountId]; -} - /// The [FetchProfileSettings] event should be triggered whenever the any user Lemmy account setting is updated. /// This event should handle any logic related to refetching the updated user preferences. class FetchProfileSettings extends ProfileEvent {} -class ResetAccountState extends ProfileEvent { - const ResetAccountState(); -} - class FetchProfileInformation extends ProfileEvent { const FetchProfileInformation({super.reload}); } diff --git a/lib/src/features/account/presentation/utils/profile_utils.dart b/lib/src/features/account/presentation/utils/profile_utils.dart index a68b2039b..9b279caa1 100644 --- a/lib/src/features/account/presentation/utils/profile_utils.dart +++ b/lib/src/features/account/presentation/utils/profile_utils.dart @@ -1,51 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/foundation/persistence/persistence.dart'; -import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/features/account/account.dart'; -import 'package:thunder/src/foundation/config/config.dart'; - -/// Fetches the currently active profile. This includes logged in and anonymous accounts. -/// -/// It will first try to find an active account. If that fails, then it will check for an anonymous account. -/// If no anonymous account is found, it will create a new default anonymous account. -Future fetchActiveProfile() async { - final prefs = UserPreferences.instance.preferences; - final accountId = prefs.getString('active_profile_id'); - - Account? account = await Account.fetchAccount(accountId ?? ''); - if (account != null) return account; - - // The user is not logged in. Let's check if there is an anonymous account. - final instance = prefs.getString(LocalSettings.currentAnonymousInstance.name); - final anonymousAccounts = await Account.anonymousInstances(); - - if (instance != null) { - account = anonymousAccounts.firstWhereOrNull((account) => account.instance == instance); - if (account != null) return account; - } - - // No default instance set. Check if there are any anonymous accounts. - if (anonymousAccounts.isNotEmpty) { - account = anonymousAccounts.first; - return account; - } - - // No anonymous account found. Let's create a new default one. TODO: Allow changing of default instance. - final defaultInstance = DEFAULT_INSTANCE; - - account = await Account.insertAnonymousInstance(Account(id: '', instance: defaultInstance, index: -1, anonymous: true, platform: ThreadiversePlatform.lemmy)); - if (account == null) throw Exception("Failed to create default profile"); - - // Set this instance as the default anonymous instance. - await prefs.setString(LocalSettings.currentAnonymousInstance.name, account.instance); - - return account; -} /// Triggers the profile modal sheet to allow selection of a different profile. Future showProfileModalSheet( @@ -56,7 +13,6 @@ Future showProfileModalSheet( bool reloadOnSwitch = true, }) async { final profileBloc = context.read(); - final thunderBloc = context.read(); await showModalBottomSheet( elevation: 0, @@ -65,7 +21,7 @@ Future showProfileModalSheet( showDragHandle: true, builder: (context) { return MultiBlocProvider( - providers: [BlocProvider.value(value: profileBloc), BlocProvider.value(value: thunderBloc)], + providers: [BlocProvider.value(value: profileBloc)], child: FractionallySizedBox( heightFactor: 0.8, child: ProfileModalBody( diff --git a/lib/src/features/account/presentation/widgets/account_page_app_bar.dart b/lib/src/features/account/presentation/widgets/account_page_app_bar.dart index 203db30d1..e16c0baa3 100644 --- a/lib/src/features/account/presentation/widgets/account_page_app_bar.dart +++ b/lib/src/features/account/presentation/widgets/account_page_app_bar.dart @@ -52,7 +52,7 @@ class _AccountPageAppBarState extends State { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final state = context.read().state; + final state = context.read().state; return BlocListener( listenWhen: (previous, current) => current.status == FeedStatus.initial, diff --git a/lib/src/features/account/presentation/widgets/profile_modal_body.dart b/lib/src/features/account/presentation/widgets/profile_modal_body.dart index 7446cbb96..a20934efb 100644 --- a/lib/src/features/account/presentation/widgets/profile_modal_body.dart +++ b/lib/src/features/account/presentation/widgets/profile_modal_body.dart @@ -12,9 +12,8 @@ import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/notification/notification.dart'; -import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/features/user/user.dart'; -import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; @@ -158,8 +157,8 @@ class _ProfileSelectState extends State { if (!darkTheme) { selectedColor = HSLColor.fromColor(theme.colorScheme.primaryContainer).withLightness(0.95).toColor(); } - Account currentAccount = context.watch().state.account; - String? currentAnonymousInstance = context.select((bloc) => bloc.state.currentAnonymousInstance); + final activeSession = context.select((bloc) => bloc.state.activeAccount); + final activeAnonymousInstance = activeSession?.anonymous == true ? activeSession!.instance : null; if (accounts == null) { fetchAccounts(); @@ -171,21 +170,9 @@ class _ProfileSelectState extends State { return MultiBlocListener( listeners: [ - BlocListener( - listener: (context, state) {}, - listenWhen: (previous, current) { - if (previous.currentAnonymousInstance != current.currentAnonymousInstance) { - anonymousInstances = null; - } - return true; - }, - ), - BlocListener( - listener: (context, state) { - if (state.status == ProfileStatus.success && state.isLoggedIn == true) { - context.read().add(const OnSetCurrentAnonymousInstance(null)); - } - }, + BlocListener( + listenWhen: (previous, current) => previous.activeAccount != current.activeAccount, + listener: (context, state) => setState(() => anonymousInstances = null), ), ], child: Scaffold( @@ -246,13 +233,13 @@ class _ProfileSelectState extends State { child: Padding( padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), child: Material( - color: currentAccount.anonymous == false && currentAccount.id == accounts![index].account.id ? selectedColor : Colors.transparent, + color: activeSession?.anonymous == false && activeSession?.id == accounts![index].account.id ? selectedColor : Colors.transparent, borderRadius: BorderRadius.circular(50), child: InkWell( - onTap: (currentAccount.id == accounts![index].account.id) + onTap: (activeSession?.id == accounts![index].account.id && activeSession?.anonymous == false) ? null : () { - context.read().add(SwitchProfile(accountId: accounts![index].account.id, reload: widget.reloadOnSave)); + context.read().add(SessionSwitched(sessionKey: accounts![index].account.id)); Navigator.of(context, rootNavigator: true).pop(); }, borderRadius: BorderRadius.circular(50), @@ -287,7 +274,7 @@ class _ProfileSelectState extends State { height: 12, child: Material( borderRadius: BorderRadius.circular(10), - color: currentAccount.id == accounts![index].account.id ? selectedColor : null, + color: activeSession?.anonymous == false && activeSession?.id == accounts![index].account.id ? selectedColor : null, ), ), ), @@ -383,7 +370,7 @@ class _ProfileSelectState extends State { trailing: !widget.quickSelectMode ? areAccountsBeingReordered ? const Icon(Icons.drag_handle) - : (currentAccount.anonymous == false && currentAccount.id == accounts![index].account.id) + : (activeSession?.anonymous == false && activeSession?.id == accounts![index].account.id) ? IconButton( icon: loggingOutId == accounts![index].account.id ? const SizedBox( @@ -406,9 +393,8 @@ class _ProfileSelectState extends State { semanticLabel: AppLocalizations.of(context)!.removeAccount, ), onPressed: () async { - context.read().add(RemoveProfile(accountId: accounts![index].account.id)); + context.read().add(SessionRemoved(sessionKey: accounts![index].account.id)); setState(() => loggingOutId = accounts![index].account.id); - context.read().add(SwitchProfile(accountId: currentAccount.id)); setState(() { accounts = null; @@ -494,12 +480,11 @@ class _ProfileSelectState extends State { padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), child: Material( elevation: anonymousInstanceBeingReorderedIndex == index ? 3 : 0, - color: currentAccount.anonymous && currentAnonymousInstance == anonymousInstances![index].anonymousInstance.instance ? selectedColor : Colors.transparent, + color: activeSession?.anonymous == true && activeAnonymousInstance == anonymousInstances![index].anonymousInstance.instance ? selectedColor : Colors.transparent, borderRadius: BorderRadius.circular(50), child: InkWell( onTap: () async { - context.read().add(SwitchProfile(accountId: anonymousInstances![index].anonymousInstance.instance)); - context.read().add(OnSetCurrentAnonymousInstance(anonymousInstances![index].anonymousInstance.instance)); + context.read().add(SessionSwitched(sessionKey: anonymousInstances![index].anonymousInstance.instance)); Navigator.of(context, rootNavigator: true).pop(); }, borderRadius: BorderRadius.circular(50), @@ -533,7 +518,7 @@ class _ProfileSelectState extends State { height: 12, child: Material( borderRadius: BorderRadius.circular(10), - color: currentAccount.anonymous && currentAnonymousInstance == anonymousInstances![index].anonymousInstance.instance ? selectedColor : null, + color: activeSession?.anonymous == true && activeAnonymousInstance == anonymousInstances![index].anonymousInstance.instance ? selectedColor : null, ), ), ), @@ -623,20 +608,11 @@ class _ProfileSelectState extends State { ? areAnonymousInstancesBeingReordered ? const Icon(Icons.drag_handle) : ((accounts?.length ?? 0) > 0 || anonymousInstances!.length > 1) - ? (currentAccount.anonymous && currentAnonymousInstance == anonymousInstances![index].anonymousInstance.instance) + ? (activeSession?.anonymous == true && activeAnonymousInstance == anonymousInstances![index].anonymousInstance.instance) ? IconButton( icon: Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.removeInstance), onPressed: () async { - await Account.deleteAnonymousInstance(anonymousInstances![index].anonymousInstance.instance); - - if (anonymousInstances!.length > 1) { - context.read().add(OnSetCurrentAnonymousInstance( - anonymousInstances!.lastWhere((instance) => instance != anonymousInstances![index]).anonymousInstance.instance)); - context.read().add( - SwitchProfile(accountId: anonymousInstances!.lastWhere((instance) => instance != anonymousInstances![index]).anonymousInstance.instance)); - } else { - context.read().add(SwitchProfile(accountId: accounts!.last.account.id)); - } + context.read().add(SessionRemoved(sessionKey: anonymousInstances![index].anonymousInstance.instance)); setState(() => anonymousInstances = null); }, @@ -647,7 +623,7 @@ class _ProfileSelectState extends State { semanticLabel: AppLocalizations.of(context)!.removeInstance, ), onPressed: () async { - await Account.deleteAnonymousInstance(anonymousInstances![index].anonymousInstance.instance); + context.read().add(SessionRemoved(sessionKey: anonymousInstances![index].anonymousInstance.instance)); setState(() { anonymousInstances = null; }); @@ -685,36 +661,17 @@ class _ProfileSelectState extends State { } Future _logOutOfActiveAccount({String? activeAccountId}) async { - activeAccountId ??= context.read().state.account.id; + final activeAccount = context.read().state.activeAccount; + activeAccountId ??= activeAccount?.anonymous == true ? activeAccount!.instance : activeAccount?.id; + if (activeAccountId == null) return; - final profileBloc = context.read(); - final ThunderBloc thunderBloc = context.read(); - - final List accountsNotCurrent = (await Account.accounts()).where((a) => a.id != activeAccountId).toList(); + final sessionBloc = context.read(); if (context.mounted && await showLogOutDialog(context)) { setState(() => loggingOutId = activeAccountId); await Future.delayed(const Duration(milliseconds: 1000), () async { - if ((anonymousInstances?.length ?? 0) > 0) { - thunderBloc.add(OnSetCurrentAnonymousInstance(anonymousInstances!.last.anonymousInstance.instance)); - profileBloc.add(SwitchProfile(accountId: anonymousInstances!.last.anonymousInstance.instance)); - } else if (accountsNotCurrent.isNotEmpty) { - profileBloc.add(SwitchProfile(accountId: accountsNotCurrent.last.id)); - } else { - // No accounts and no anonymous instances left. Create a new one. - await Account.insertAnonymousInstance(Account( - id: '', - instance: DEFAULT_INSTANCE, - index: -1, - anonymous: true, - platform: ThreadiversePlatform.lemmy, - )); - - thunderBloc.add(const OnSetCurrentAnonymousInstance(null)); - thunderBloc.add(const OnSetCurrentAnonymousInstance(DEFAULT_INSTANCE)); - profileBloc.add(SwitchProfile(accountId: DEFAULT_INSTANCE)); - } + sessionBloc.add(SessionRemoved(sessionKey: activeAccountId!)); setState(() { accounts = null; diff --git a/lib/src/features/comment/presentation/pages/create_comment_page.dart b/lib/src/features/comment/presentation/pages/create_comment_page.dart index bc02d70f2..f77c85ed8 100644 --- a/lib/src/features/comment/presentation/pages/create_comment_page.dart +++ b/lib/src/features/comment/presentation/pages/create_comment_page.dart @@ -11,13 +11,13 @@ import 'package:markdown_editor/markdown_editor.dart'; // Project imports import 'package:thunder/src/features/account/account.dart'; -import 'package:thunder/src/app/wiring/state_factories.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/drafts/drafts.dart'; import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/post/post.dart'; +import 'package:thunder/src/features/session/session.dart'; import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; import 'package:thunder/src/shared/input_dialogs.dart'; import 'package:thunder/src/shared/language_selector.dart'; @@ -61,12 +61,6 @@ class CreateCommentPage extends StatefulWidget { } class _CreateCommentPageState extends State with WidgetsBindingObserver { - /// The current account - Account? account; - - /// The account's user information - ThunderUser? user; - final DraftRepository _draftRepository = DraftRepositoryImpl(database: database); /// Whether to save this comment as a draft @@ -102,12 +96,21 @@ class _CreateCommentPageState extends State with WidgetsBindi /// Whether the user was temporarily changed to create the comment bool userChanged = false; + /// The id of the account for which the draft is being saved. This is used to determine which draft to restore when the page is opened. + String? _draftAccountId; + /// The ID of the post we're responding to int? postId; /// The ID of the comment we're responding to int? parentCommentId; + /// The post currently targeted by the reply flow. + ThunderPost? post; + + /// The parent comment currently targeted by the reply flow. + ThunderComment? parentComment; + /// Contains the text that is currently being selected in the post/comment that we are replying to String? replyViewSelection; @@ -117,10 +120,13 @@ class _CreateCommentPageState extends State with WidgetsBindi WidgetsBinding.instance.addObserver(this); - account = widget.account ?? context.read().state.account; + _draftAccountId = widget.account?.id ?? context.read().state.effectiveAccount.id; - postId = widget.post?.id ?? widget.parentComment?.postId; - parentCommentId = widget.parentComment?.id; + post = widget.post; + parentComment = widget.parentComment; + + postId = post?.id ?? parentComment?.postId; + parentCommentId = parentComment?.id; _bodyTextController.addListener(() { _validateSubmission(); @@ -212,7 +218,7 @@ class _CreateCommentPageState extends State with WidgetsBindi Draft _buildDraft() => buildCommentDraft( context: _draftContext, - accountId: account?.id, + accountId: _draftAccountId, languageId: languageId, body: _bodyTextController.text, ); @@ -248,304 +254,318 @@ class _CreateCommentPageState extends State with WidgetsBindi return PopScope( onPopInvokedWithResult: (didPop, result) {}, - child: BlocProvider( - create: (context) => createCreateCommentCubit(account!), - child: BlocConsumer( - listener: (ctx, state) { - if (state.status == CreateCommentStatus.success && state.comment != null) { - widget.onCommentSuccess?.call(state.comment!, userChanged); - Navigator.of(context).pop(state.comment); - } - - if (state.status == CreateCommentStatus.error && state.message != null) { - showSnackbar(state.message!); - ctx.read().clearMessage(); - } - - switch (state.status) { - case CreateCommentStatus.imageUploadSuccess: - String markdownImages = state.imageUrls?.map((url) => '![]($url)').join('\n\n') ?? ''; - _bodyTextController.text = _bodyTextController.text.replaceRange(_bodyTextController.selection.end, _bodyTextController.selection.end, markdownImages); - break; - case CreateCommentStatus.imageUploadFailure: - showSnackbar(l10n.postUploadImageError, leadingIcon: Icons.warning_rounded, leadingIconColor: theme.colorScheme.errorContainer); - default: - break; - } - }, - builder: (context, state) { - return GestureDetector( - onTap: () { - FocusManager.instance.primaryFocus?.unfocus(); - }, - child: Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - title: Text(widget.comment != null ? l10n.editComment : l10n.createComment), - toolbarHeight: APP_BAR_HEIGHT, - centerTitle: false, - ), - body: SafeArea( - bottom: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.post != null) - Padding( - padding: const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 16.0), - child: Container( - padding: const EdgeInsets.only(top: 6.0, bottom: 12.0), - decoration: BoxDecoration( - color: getBackgroundColor(context), - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - ), - child: PostBody( - post: widget.post!, - crossPosts: const [], - viewSource: viewSource, - onViewSourceToggled: () => setState(() => viewSource = !viewSource), - showQuickPostActionBar: false, - selectable: true, - showReplyEditorButtons: true, - onSelectionChanged: (selection) => replyViewSelection = selection, - ), - ), - ), - if (widget.parentComment != null) ...[ - Padding( - padding: const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 16.0), - child: Container( - decoration: BoxDecoration( - color: getBackgroundColor(context), - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - ), - child: CommentContent( - account: account!, - comment: widget.parentComment!, - hidden: false, - viewSource: viewSource, - onViewSourceToggled: () => setState(() => viewSource = !viewSource), - selectable: true, - showReplyEditorButtons: true, - onSelectionChanged: (selection) => replyViewSelection = selection, - ), - ), - ), - ], - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + child: BlocConsumer( + listener: (context, featureAccountState) { + _draftAccountId = featureAccountState.effectiveAccount.id; + context.read().switchAccount(featureAccountState.effectiveAccount); + }, + builder: (context, featureAccountState) { + final account = featureAccountState.effectiveAccount; + + return BlocConsumer( + listener: (ctx, state) { + if (state.status == CreateCommentStatus.success && state.comment != null) { + widget.onCommentSuccess?.call(state.comment!, userChanged); + Navigator.of(context).pop(state.comment); + } + + if (state.status == CreateCommentStatus.error && state.message != null) { + showSnackbar(state.message!); + ctx.read().clearMessage(); + } + + switch (state.status) { + case CreateCommentStatus.imageUploadSuccess: + String markdownImages = state.imageUrls?.map((url) => '![]($url)').join('\n\n') ?? ''; + _bodyTextController.text = _bodyTextController.text.replaceRange(_bodyTextController.selection.end, _bodyTextController.selection.end, markdownImages); + break; + case CreateCommentStatus.imageUploadFailure: + showSnackbar(l10n.postUploadImageError, leadingIcon: Icons.warning_rounded, leadingIconColor: theme.colorScheme.errorContainer); + default: + break; + } + }, + builder: (context, state) { + return GestureDetector( + onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); + }, + child: Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: Text(widget.comment != null ? l10n.editComment : l10n.createComment), + toolbarHeight: APP_BAR_HEIGHT, + centerTitle: false, + ), + body: SafeArea( + bottom: false, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (post != null) Padding( - padding: const EdgeInsets.only(left: 16.0), - child: UserSelector( - account: account!, - postActorId: widget.post?.apId, - onPostChanged: (ThunderPost post) { - postId = post.id; - _onDraftInputChanged(); - }, - parentCommentActorId: widget.parentComment?.apId, - onParentCommentChanged: (ThunderComment parentComment) { - postId = parentComment.postId; - parentCommentId = parentComment.id; - _onDraftInputChanged(); - }, - onUserChanged: (account) { - setState(() { - userChanged = this.account?.instance != account.instance; - this.account = account; - }); - - context.read().switchAccount(account); - _onDraftInputChanged(); - }, - enableAccountSwitching: widget.comment == null, + padding: const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 16.0), + child: Container( + padding: const EdgeInsets.only(top: 6.0, bottom: 12.0), + decoration: BoxDecoration( + color: getBackgroundColor(context), + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + ), + child: PostBody( + post: post!, + crossPosts: const [], + viewSource: viewSource, + onViewSourceToggled: () => setState(() => viewSource = !viewSource), + showQuickPostActionBar: false, + selectable: true, + showReplyEditorButtons: true, + onSelectionChanged: (selection) => replyViewSelection = selection, + ), ), ), - const SizedBox(height: 10), + if (parentComment != null) ...[ Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: LanguageSelector( - languageId: languageId, - onLanguageSelected: (ThunderLanguage? language) { - setState(() => languageId = language?.id); - _onDraftInputChanged(); - }, + padding: const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 16.0), + child: Container( + decoration: BoxDecoration( + color: getBackgroundColor(context), + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + ), + child: CommentContent( + account: account, + comment: parentComment!, + hidden: false, + viewSource: viewSource, + onViewSourceToggled: () => setState(() => viewSource = !viewSource), + selectable: true, + showReplyEditorButtons: true, + onSelectionChanged: (selection) => replyViewSelection = selection, + ), ), ), - const SizedBox(height: 10), - AnimatedCrossFade( - firstChild: Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: getBackgroundColor(context), - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - ), - child: CommonMarkdownBody(body: _bodyTextController.text, isComment: true), + ], + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: UserSelector( + account: account, + postActorId: post?.apId, + onPostChanged: (ThunderPost post) { + setState(() { + this.post = post; + postId = post.id; + }); + _onDraftInputChanged(); + }, + parentCommentActorId: parentComment?.apId, + onParentCommentChanged: (ThunderComment parentComment) { + setState(() { + this.parentComment = parentComment; + postId = parentComment.postId; + parentCommentId = parentComment.id; + }); + _onDraftInputChanged(); + }, + onUserChanged: (account) { + setState(() { + userChanged = featureAccountState.effectiveAccount.id != account.id; + _draftAccountId = account.id; + }); + + context.read().setOverride(account); + _onDraftInputChanged(); + }, + enableAccountSwitching: widget.comment == null, ), ), - secondChild: Padding( + const SizedBox(height: 10), + Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: MarkdownTextInputField( - controller: _bodyTextController, - focusNode: _bodyFocusNode, - label: l10n.comment, - minLines: 8, - maxLines: null, - textStyle: theme.textTheme.bodyLarge, - spellCheckConfiguration: const SpellCheckConfiguration.disabled(), + child: LanguageSelector( + account: account, + languageId: languageId, + onLanguageSelected: (ThunderLanguage? language) { + setState(() => languageId = language?.id); + _onDraftInputChanged(); + }, ), ), - crossFadeState: showPreview ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 120), - excludeBottomFocus: false, - ) - ], - ), - ], + const SizedBox(height: 10), + AnimatedCrossFade( + firstChild: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: getBackgroundColor(context), + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + ), + child: CommonMarkdownBody(body: _bodyTextController.text, isComment: true), + ), + ), + secondChild: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: MarkdownTextInputField( + controller: _bodyTextController, + focusNode: _bodyFocusNode, + label: l10n.comment, + minLines: 8, + maxLines: null, + textStyle: theme.textTheme.bodyLarge, + spellCheckConfiguration: const SpellCheckConfiguration.disabled(), + ), + ), + crossFadeState: showPreview ? CrossFadeState.showFirst : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 120), + excludeBottomFocus: false, + ) + ], + ), + ], + ), ), ), - ), - const Divider(height: 1), - Container( - color: theme.cardColor, - margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - child: Row( - children: [ - Expanded( - child: IgnorePointer( - ignoring: showPreview, - child: MarkdownToolbar( - controller: _bodyTextController, - focusNode: _bodyFocusNode, - actions: const [ - MarkdownType.image, - MarkdownType.link, - MarkdownType.bold, - MarkdownType.italic, - MarkdownType.blockquote, - MarkdownType.strikethrough, - MarkdownType.title, - MarkdownType.list, - MarkdownType.separator, - MarkdownType.code, - MarkdownType.spoiler, - MarkdownType.username, - MarkdownType.community, - ], - customTapActions: { - MarkdownType.username: () { - showUserInputDialog( - context, - title: l10n.username, - account: account!, - onUserSelected: (ThunderUser user) { - _bodyTextController.text = _bodyTextController.text.replaceRange( - _bodyTextController.selection.end, - _bodyTextController.selection.end, - '[@${user.name}@${fetchInstanceNameFromUrl(user.actorId)}](${user.actorId})', - ); - }, - ); + const Divider(height: 1), + Container( + color: theme.cardColor, + margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Row( + children: [ + Expanded( + child: IgnorePointer( + ignoring: showPreview, + child: MarkdownToolbar( + controller: _bodyTextController, + focusNode: _bodyFocusNode, + actions: const [ + MarkdownType.image, + MarkdownType.link, + MarkdownType.bold, + MarkdownType.italic, + MarkdownType.blockquote, + MarkdownType.strikethrough, + MarkdownType.title, + MarkdownType.list, + MarkdownType.separator, + MarkdownType.code, + MarkdownType.spoiler, + MarkdownType.username, + MarkdownType.community, + ], + customTapActions: { + MarkdownType.username: () { + showUserInputDialog( + context, + title: l10n.username, + account: account, + onUserSelected: (ThunderUser user) { + _bodyTextController.text = _bodyTextController.text.replaceRange( + _bodyTextController.selection.end, + _bodyTextController.selection.end, + '[@${user.name}@${fetchInstanceNameFromUrl(user.actorId)}](${user.actorId})', + ); + }, + ); + }, + MarkdownType.community: () { + showCommunityInputDialog( + context, + title: l10n.community, + account: account, + onCommunitySelected: (ThunderCommunity community) { + _bodyTextController.text = _bodyTextController.text.replaceRange( + _bodyTextController.selection.end, + _bodyTextController.selection.end, + '!${community.name}@${fetchInstanceNameFromUrl(community.actorId)}', + ); + }, + ); + }, }, - MarkdownType.community: () { - showCommunityInputDialog( - context, - title: l10n.community, - account: account!, - onCommunitySelected: (ThunderCommunity community) { - _bodyTextController.text = _bodyTextController.text.replaceRange( - _bodyTextController.selection.end, - _bodyTextController.selection.end, - '!${community.name}@${fetchInstanceNameFromUrl(community.actorId)}', - ); - }, - ); + imageIsLoading: state.status == CreateCommentStatus.imageUploadInProgress, + customImageButtonAction: () async { + if (state.status == CreateCommentStatus.imageUploadInProgress) { + return; + } + + List imagesPath = await selectImagesToUpload(allowMultiple: true); + if (context.mounted) { + context.read().uploadImages(imagesPath); + } }, - }, - imageIsLoading: state.status == CreateCommentStatus.imageUploadInProgress, - customImageButtonAction: () async { - if (state.status == CreateCommentStatus.imageUploadInProgress) { - return; + getAlternativeSelection: () => replyViewSelection, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 2.0, top: 2.0, left: 4.0, right: 2.0), + child: IconButton( + onPressed: () { + if (!showPreview) { + setState(() => wasKeyboardVisible = keyboardVisibilityController.isVisible); + FocusManager.instance.primaryFocus?.unfocus(); } - List imagesPath = await selectImagesToUpload(allowMultiple: true); - if (context.mounted) { - context.read().uploadImages(imagesPath); + setState(() => showPreview = !showPreview); + if (!showPreview && wasKeyboardVisible) { + _bodyFocusNode.requestFocus(); } }, - getAlternativeSelection: () => replyViewSelection, - ), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 2.0, top: 2.0, left: 4.0, right: 2.0), - child: IconButton( - onPressed: () { - if (!showPreview) { - setState(() => wasKeyboardVisible = keyboardVisibilityController.isVisible); - FocusManager.instance.primaryFocus?.unfocus(); - } - - setState(() => showPreview = !showPreview); - if (!showPreview && wasKeyboardVisible) { - _bodyFocusNode.requestFocus(); - } - }, - icon: Icon( - showPreview ? Icons.visibility_off_rounded : Icons.visibility, - color: theme.colorScheme.onSecondary, - semanticLabel: l10n.postTogglePreview, + icon: Icon( + showPreview ? Icons.visibility_off_rounded : Icons.visibility, + color: theme.colorScheme.onSecondary, + semanticLabel: l10n.postTogglePreview, + ), + style: ElevatedButton.styleFrom(backgroundColor: theme.colorScheme.secondaryContainer), ), - style: ElevatedButton.styleFrom(backgroundColor: theme.colorScheme.secondaryContainer), ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 2.0, top: 2.0, left: 2.0, right: 8.0), - child: SizedBox( - width: 60, - child: IconButton( - onPressed: isSubmitButtonDisabled || state.status == CreateCommentStatus.submitting ? null : () => _onCreateComment(context), - icon: state.status == CreateCommentStatus.submitting - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(), - ) - : Icon( - widget.comment != null ? Icons.edit_rounded : Icons.send_rounded, - color: theme.colorScheme.onSecondary, - semanticLabel: widget.comment != null ? l10n.editComment : l10n.createComment, - ), - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.secondary, - disabledBackgroundColor: getBackgroundColor(context), + Padding( + padding: const EdgeInsets.only(bottom: 2.0, top: 2.0, left: 2.0, right: 8.0), + child: SizedBox( + width: 60, + child: IconButton( + onPressed: isSubmitButtonDisabled || state.status == CreateCommentStatus.submitting ? null : () => _onCreateComment(context), + icon: state.status == CreateCommentStatus.submitting + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), + ) + : Icon( + widget.comment != null ? Icons.edit_rounded : Icons.send_rounded, + color: theme.colorScheme.onSecondary, + semanticLabel: widget.comment != null ? l10n.editComment : l10n.createComment, + ), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.secondary, + disabledBackgroundColor: getBackgroundColor(context), + ), ), ), ), - ), - ], + ], + ), ), - ), - Container( - height: MediaQuery.of(context).padding.bottom, - color: theme.cardColor, - ), - ], + Container( + height: MediaQuery.of(context).padding.bottom, + color: theme.cardColor, + ), + ], + ), ), ), - ), - ); - }, - ), + ); + }, + ); + }, ), ); } diff --git a/lib/src/features/comment/presentation/utils/comment_utils.dart b/lib/src/features/comment/presentation/utils/comment_utils.dart index e9112baa9..4a3e4b36f 100644 --- a/lib/src/features/comment/presentation/utils/comment_utils.dart +++ b/lib/src/features/comment/presentation/utils/comment_utils.dart @@ -180,10 +180,10 @@ Future onCommentAction(BuildContext context, Account account, C await repository.report(comment.id, data?['reason']); break; case CommentAction.reply: - updatedComment = await navigateToCreateCommentPage(context, parentComment: comment, onCommentSuccess: (comment, _) => updatedComment = comment); + updatedComment = await navigateToCreateCommentPage(context, account: account, parentComment: comment, onCommentSuccess: (comment, _) => updatedComment = comment); break; case CommentAction.edit: - updatedComment = await navigateToCreateCommentPage(context, comment: comment, onCommentSuccess: (comment, _) => updatedComment = comment); + updatedComment = await navigateToCreateCommentPage(context, account: account, comment: comment, onCommentSuccess: (comment, _) => updatedComment = comment); break; case CommentAction.remove: // TODO: Handle this case. diff --git a/lib/src/features/comment/presentation/widgets/comment_bottom_sheet/comment_action_bottom_sheet.dart b/lib/src/features/comment/presentation/widgets/comment_bottom_sheet/comment_action_bottom_sheet.dart index 479035506..e8d3d5778 100644 --- a/lib/src/features/comment/presentation/widgets/comment_bottom_sheet/comment_action_bottom_sheet.dart +++ b/lib/src/features/comment/presentation/widgets/comment_bottom_sheet/comment_action_bottom_sheet.dart @@ -3,13 +3,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:back_button_interceptor/back_button_interceptor.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/settings/api.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/features/account/data/cache/profile_site_info_cache.dart'; import 'package:thunder/src/features/instance/instance.dart'; import 'package:thunder/src/shared/share/share_action_bottom_sheet.dart'; @@ -28,7 +28,10 @@ void showCommentActionBottomModalSheet( context: context, showDragHandle: true, isScrollControlled: true, - builder: (_) => CommentActionBottomSheet(context: context, initialPage: page, comment: comment, onAction: onAction, isShowingSource: isShowingSource), + builder: (_) => wrapWithCapturedAccountContext( + context, + CommentActionBottomSheet(context: context, initialPage: page, comment: comment, onAction: onAction, isShowingSource: isShowingSource), + ), ); } @@ -85,7 +88,7 @@ class _CommentActionBottomSheetState extends State { @override void initState() { super.initState(); - account = context.read().state.account; + account = resolveEffectiveAccount(widget.context); currentPage = widget.initialPage; BackButtonInterceptor.add(_handleBack); @@ -167,6 +170,7 @@ class _CommentActionBottomSheetState extends State { }, ), GeneralCommentAction.instance => InstanceActionBottomSheet( + context: widget.context, account: account, blockedInstances: blockedInstances, userInstanceId: widget.comment.creator!.instanceId, @@ -212,7 +216,7 @@ class _CommentActionBottomSheetState extends State { if (currentPage == GeneralCommentAction.general) Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), - child: LanguagePostCardMetaData(languageId: widget.comment.languageId), + child: LanguagePostCardMetaData(languageId: widget.comment.languageId, account: account), ), const SizedBox(height: 16.0), actions, diff --git a/lib/src/features/comment/presentation/widgets/comment_bottom_sheet/comment_comment_action_bottom_sheet.dart b/lib/src/features/comment/presentation/widgets/comment_bottom_sheet/comment_comment_action_bottom_sheet.dart index 3dfc1fd18..ba562d811 100644 --- a/lib/src/features/comment/presentation/widgets/comment_bottom_sheet/comment_comment_action_bottom_sheet.dart +++ b/lib/src/features/comment/presentation/widgets/comment_bottom_sheet/comment_comment_action_bottom_sheet.dart @@ -123,7 +123,7 @@ class _CommentCommentActionBottomSheetState extends State { Padding( padding: const EdgeInsets.only(bottom: 4.0, top: 6.0, right: 4.0), child: CommentCardButtonActions( + account: widget.account, comment: comment, isOwnComment: isOwnComment, onAction: (action) => _onAction(action), diff --git a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_button_actions.dart b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_button_actions.dart index 4eb21239f..8c7f63979 100644 --- a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_button_actions.dart +++ b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_button_actions.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thunder/src/features/account/data/cache/profile_site_info_cache.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/account/account.dart'; @@ -11,7 +12,9 @@ import 'package:thunder/src/foundation/config/global_context.dart'; /// Displays a row of actions that can be performed on a comment. /// /// This is only shown when comment button actions are enabled. -class CommentCardButtonActions extends StatelessWidget { +class CommentCardButtonActions extends StatefulWidget { + final Account account; + /// The comment to perform actions on final ThunderComment comment; @@ -26,20 +29,51 @@ class CommentCardButtonActions extends StatelessWidget { const CommentCardButtonActions({ super.key, + required this.account, required this.comment, required this.isOwnComment, required this.onAction, required this.onBottomSheetOpen, }); + @override + State createState() => _CommentCardButtonActionsState(); +} + +class _CommentCardButtonActionsState extends State { + bool _downvotesEnabled = true; + + @override + void initState() { + super.initState(); + _loadSiteInfo(); + } + + @override + void didUpdateWidget(covariant CommentCardButtonActions oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.account.id != widget.account.id || oldWidget.account.instance != widget.account.instance || oldWidget.account.anonymous != widget.account.anonymous) { + _loadSiteInfo(); + } + } + + Future _loadSiteInfo() async { + if (widget.account.anonymous) { + if (!mounted) return; + setState(() => _downvotesEnabled = true); + return; + } + + final siteInfo = await ProfileSiteInfoCache.instance.get(widget.account); + if (!mounted) return; + setState(() => _downvotesEnabled = siteInfo.site.enableDownvotes ?? true); + } + @override Widget build(BuildContext context) { final l10n = GlobalContext.l10n; - // TODO: Check if the account's instance has downvotes enabled rather than the current profile bloc - final downvotesEnabled = context.select((bloc) => bloc.state.downvotesEnabled); - - final voteType = comment.myVote ?? 0; + final voteType = widget.comment.myVote ?? 0; final upvoteColor = context.select((cubit) => cubit.state.upvoteColor.color); final downvoteColor = context.select((cubit) => cubit.state.downvoteColor.color); @@ -47,16 +81,16 @@ class CommentCardButtonActions extends StatelessWidget { _CommentCardButtonAction( icon: Icons.more_horiz_rounded, label: l10n.actions, - onAction: onBottomSheetOpen, + onAction: widget.onBottomSheetOpen, ) ]; - if (isOwnComment) { + if (widget.isOwnComment) { widgets.add( _CommentCardButtonAction( icon: Icons.edit_rounded, label: l10n.edit, - onAction: () => onAction(SwipeAction.edit), + onAction: () => widget.onAction(SwipeAction.edit), ), ); } else { @@ -64,7 +98,7 @@ class CommentCardButtonActions extends StatelessWidget { _CommentCardButtonAction( icon: Icons.reply_rounded, label: l10n.reply(1), - onAction: () => onAction(SwipeAction.reply), + onAction: () => widget.onAction(SwipeAction.reply), ), ); } @@ -74,17 +108,17 @@ class CommentCardButtonActions extends StatelessWidget { icon: Icons.arrow_upward, label: voteType == 1 ? l10n.upvoted : l10n.upvote, color: voteType == 1 ? upvoteColor : null, - onAction: () => onAction(SwipeAction.upvote), + onAction: () => widget.onAction(SwipeAction.upvote), ), ); - if (downvotesEnabled) { + if (_downvotesEnabled) { widgets.add( _CommentCardButtonAction( icon: Icons.arrow_downward, label: voteType == -1 ? l10n.downvoted : l10n.downvote, color: voteType == -1 ? downvoteColor : null, - onAction: () => onAction(SwipeAction.downvote), + onAction: () => widget.onAction(SwipeAction.downvote), ), ); } diff --git a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header.dart b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header.dart index 376990d4b..eb20e6df9 100644 --- a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header.dart +++ b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header.dart @@ -64,7 +64,7 @@ class CommentCardHeader extends StatelessWidget { opacity: 1.0, constraints: constraints, ), - CommentCardHeaderScore(score: comment.score!, upvotes: comment.upvotes!, downvotes: comment.downvotes!, voteType: comment.myVote), + CommentCardHeaderScore(account: account, score: comment.score!, upvotes: comment.upvotes!, downvotes: comment.downvotes!, voteType: comment.myVote), ], ), Row( diff --git a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_score.dart b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_score.dart index 08c7f6f89..271467422 100644 --- a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_score.dart +++ b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_score.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thunder/src/features/account/data/cache/profile_site_info_cache.dart'; import 'package:thunder/src/features/comment/api.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; @@ -14,7 +15,9 @@ import 'package:thunder/packages/ui/ui.dart' show ScalableText; /// /// The widget will display the combined score if [combineCommentScores] is true. Otherwise, it will display the votes separately. /// If [showScores] is false, only the vote indicator (upvote/downvote) will be shown. -class CommentCardHeaderScore extends StatelessWidget { +class CommentCardHeaderScore extends StatefulWidget { + final Account account; + /// The combined score final int score; @@ -29,54 +32,86 @@ class CommentCardHeaderScore extends StatelessWidget { const CommentCardHeaderScore({ super.key, + required this.account, required this.score, required this.upvotes, required this.downvotes, this.voteType, }); + @override + State createState() => _CommentCardHeaderScoreState(); +} + +class _CommentCardHeaderScoreState extends State { + bool _showScores = true; + + @override + void initState() { + super.initState(); + _loadSiteInfo(); + } + + @override + void didUpdateWidget(covariant CommentCardHeaderScore oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.account.id != widget.account.id || oldWidget.account.instance != widget.account.instance || oldWidget.account.anonymous != widget.account.anonymous) { + _loadSiteInfo(); + } + } + + Future _loadSiteInfo() async { + if (widget.account.anonymous) { + if (!mounted) return; + setState(() => _showScores = true); + return; + } + + final siteInfo = await ProfileSiteInfoCache.instance.get(widget.account); + if (!mounted) return; + setState(() => _showScores = siteInfo.myUser?.localUserView.localUser.showScores ?? true); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); final l10n = GlobalContext.l10n; - final showScores = context.select((ProfileBloc bloc) => bloc.state.siteResponse?.myUser?.localUserView.localUser.showScores) ?? true; - final metadataFontSizeScale = context.select((cubit) => cubit.state.metadataFontSizeScale); final combineCommentScores = context.select((cubit) => cubit.state.combineCommentScores); final upvoteColor = context.select((cubit) => cubit.state.upvoteColor.color); final downvoteColor = context.select((cubit) => cubit.state.downvoteColor.color); // Show only vote indicator if scores are hidden - if (!showScores) { - if (voteType == 1) return VoteIcon(type: voteType!, voteType: voteType, color: upvoteColor, fontScale: metadataFontSizeScale); - if (voteType == -1) return VoteIcon(type: voteType!, voteType: voteType, color: downvoteColor, fontScale: metadataFontSizeScale); + if (!_showScores) { + if (widget.voteType == 1) return VoteIcon(type: widget.voteType!, voteType: widget.voteType, color: upvoteColor, fontScale: metadataFontSizeScale); + if (widget.voteType == -1) return VoteIcon(type: widget.voteType!, voteType: widget.voteType, color: downvoteColor, fontScale: metadataFontSizeScale); return SizedBox.shrink(); } - final scoreLabel = formatNumberToK(score); - final upvotesLabel = formatNumberToK(upvotes); - final downvotesLabel = formatNumberToK(downvotes); + final scoreLabel = formatNumberToK(widget.score); + final upvotesLabel = formatNumberToK(widget.upvotes); + final downvotesLabel = formatNumberToK(widget.downvotes); // Show the combined score if (combineCommentScores) { return Row( spacing: 2.0, children: [ - VoteIcon(type: 1, voteType: voteType, color: upvoteColor, fontScale: metadataFontSizeScale), + VoteIcon(type: 1, voteType: widget.voteType, color: upvoteColor, fontScale: metadataFontSizeScale), ScalableText( scoreLabel, semanticsLabel: l10n.xScore(scoreLabel), textScaleFactor: metadataFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( - color: (voteType != null && voteType != 0) - ? voteType == 1 + color: (widget.voteType != null && widget.voteType != 0) + ? widget.voteType == 1 ? upvoteColor : downvoteColor : theme.colorScheme.onSurface, ), ), - VoteIcon(type: -1, voteType: voteType, color: downvoteColor, fontScale: metadataFontSizeScale), + VoteIcon(type: -1, voteType: widget.voteType, color: downvoteColor, fontScale: metadataFontSizeScale), ], ); } @@ -85,26 +120,26 @@ class CommentCardHeaderScore extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - VoteIcon(type: 1, voteType: voteType, color: upvoteColor, fontScale: metadataFontSizeScale), + VoteIcon(type: 1, voteType: widget.voteType, color: upvoteColor, fontScale: metadataFontSizeScale), const SizedBox(width: 2.0), ScalableText( upvotesLabel, semanticsLabel: l10n.xUpvotes(upvotesLabel), textScaleFactor: metadataFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( - color: (voteType == 1) ? upvoteColor : theme.colorScheme.onSurface, + color: (widget.voteType == 1) ? upvoteColor : theme.colorScheme.onSurface, ), ), const SizedBox(width: 10.0), - if (downvotes != 0) ...[ - VoteIcon(type: -1, voteType: voteType, color: downvoteColor, fontScale: metadataFontSizeScale), + if (widget.downvotes != 0) ...[ + VoteIcon(type: -1, voteType: widget.voteType, color: downvoteColor, fontScale: metadataFontSizeScale), const SizedBox(width: 2.0), ScalableText( downvotesLabel, semanticsLabel: l10n.xDownvotes(downvotesLabel), textScaleFactor: metadataFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( - color: (voteType == -1) ? downvoteColor : theme.colorScheme.onSurface, + color: (widget.voteType == -1) ? downvoteColor : theme.colorScheme.onSurface, ), ), ], diff --git a/lib/src/features/comment/presentation/widgets/comment_list_entry.dart b/lib/src/features/comment/presentation/widgets/comment_list_entry.dart index feb6af815..ecf34ebb7 100644 --- a/lib/src/features/comment/presentation/widgets/comment_list_entry.dart +++ b/lib/src/features/comment/presentation/widgets/comment_list_entry.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/app/wiring/state_factories.dart'; -import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/src/features/post/post.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/features/comment/presentation/widgets/comment_reference.dart'; /// A widget that can display a single comment entry for use within a list (e.g., search page, instance explorer) @@ -17,7 +17,7 @@ class CommentListEntry extends StatelessWidget { @override Widget build(BuildContext context) { - final account = context.select((bloc) => bloc.state.account); + final account = resolveEffectiveAccount(context); return BlocProvider( create: (BuildContext context) => createPostBloc(account), diff --git a/lib/src/features/comment/presentation/widgets/comment_reference.dart b/lib/src/features/comment/presentation/widgets/comment_reference.dart index f0401843e..4a4194dad 100644 --- a/lib/src/features/comment/presentation/widgets/comment_reference.dart +++ b/lib/src/features/comment/presentation/widgets/comment_reference.dart @@ -6,6 +6,7 @@ import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/account/api.dart'; import 'package:thunder/src/features/comment/api.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/packages/ui/ui.dart' show ScalableText; @@ -45,7 +46,7 @@ class CommentReference extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = GlobalContext.l10n; - final account = context.read().state.account; + final account = resolveEffectiveAccount(context); assert(comment.creator != null && comment.community != null && comment.post != null, 'Comment must have creator, community, and post fields'); diff --git a/lib/src/features/community/community.dart b/lib/src/features/community/community.dart index 041bd1046..4ed36ca26 100644 --- a/lib/src/features/community/community.dart +++ b/lib/src/features/community/community.dart @@ -2,7 +2,7 @@ export 'domain/enums/community_action.dart'; export 'domain/models/community_details.dart'; export 'package:thunder/src/foundation/primitives/models/thunder_community.dart'; export 'data/repositories/community_repository_impl.dart'; -export 'presentation/state/anonymous_subscriptions_bloc.dart'; +export 'presentation/state/anonymous_subscriptions_cubit.dart'; export 'presentation/widgets/community_drawer.dart'; export 'presentation/widgets/community_header/community_header.dart'; export 'presentation/widgets/community_header/community_header_actions.dart'; diff --git a/lib/src/features/community/data/data_sources/favorite_local_data_source.dart b/lib/src/features/community/data/data_sources/favorite_local_data_source.dart index 8ccdad9bb..3c356acd7 100644 --- a/lib/src/features/community/data/data_sources/favorite_local_data_source.dart +++ b/lib/src/features/community/data/data_sources/favorite_local_data_source.dart @@ -39,7 +39,9 @@ class Favorite { // A method that retrieves all favourites from the database static Future> favorites(String accountId) async { try { - return (await database.favorites.all().get()).map((favorite) => Favorite(id: favorite.id.toString(), accountId: favorite.accountId.toString(), communityId: favorite.communityId)).toList(); + return (await (database.select(database.favorites)..where((table) => table.accountId.equals(int.parse(accountId)))).get()) + .map((favorite) => Favorite(id: favorite.id.toString(), accountId: favorite.accountId.toString(), communityId: favorite.communityId)) + .toList(); } catch (e) { debugPrint(e.toString()); return []; diff --git a/lib/src/features/community/presentation/state/anonymous_subscriptions_bloc.dart b/lib/src/features/community/presentation/state/anonymous_subscriptions_cubit.dart similarity index 51% rename from lib/src/features/community/presentation/state/anonymous_subscriptions_bloc.dart rename to lib/src/features/community/presentation/state/anonymous_subscriptions_cubit.dart index 249105f6b..d7e3d073d 100644 --- a/lib/src/features/community/presentation/state/anonymous_subscriptions_bloc.dart +++ b/lib/src/features/community/presentation/state/anonymous_subscriptions_cubit.dart @@ -1,40 +1,59 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; -import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; -import 'package:stream_transform/stream_transform.dart'; -import 'package:thunder/src/foundation/errors/errors.dart'; import 'package:thunder/src/features/community/community.dart'; - +import 'package:thunder/src/foundation/errors/errors.dart'; import 'package:thunder/src/foundation/networking/networking.dart'; -part 'anonymous_subscriptions_event.dart'; part 'anonymous_subscriptions_state.dart'; -const throttleDuration = Duration(seconds: 1); +const _anonymousSubscriptionsThrottleDuration = Duration(seconds: 1); -EventTransformer throttleDroppable(Duration duration) { - return (events, mapper) => droppable().call(events.throttle(duration), mapper); -} +class AnonymousSubscriptionsCubit extends Cubit { + AnonymousSubscriptionsCubit() : super(const AnonymousSubscriptionsState()); + + DateTime? _lastLoadAt; + Future? _pendingLoad; + + Future loadSubscribedCommunities() { + final now = DateTime.now(); + + if (_pendingLoad != null) { + return _pendingLoad!; + } + + if (_lastLoadAt != null && now.difference(_lastLoadAt!) < _anonymousSubscriptionsThrottleDuration) { + return Future.value(); + } -class AnonymousSubscriptionsBloc extends Bloc { - AnonymousSubscriptionsBloc() : super(const AnonymousSubscriptionsState()) { - on(_getSubscribedCommunities, transformer: throttleDroppable(throttleDuration)); - on(_addSubscriptions); - on(_deleteSubscriptions); + final future = _loadSubscribedCommunities(); + _pendingLoad = future; + + future.whenComplete(() { + if (identical(_pendingLoad, future)) { + _pendingLoad = null; + } + }); + + return future; } - FutureOr _deleteSubscriptions(DeleteSubscriptionsEvent event, Emitter emit) async { + Future addSubscriptions(Set communities) async { try { - await AnonymousSubscriptions.deleteCommunities(event.urls); + final newCommunities = communities.where((ThunderCommunity community) => !state.urls.contains(community.actorId)).toList(); + if (newCommunities.isEmpty) return; + + await insertSubscriptions(newCommunities.toSet()); emit( state.copyWith( status: AnonymousSubscriptionsStatus.success, - subscriptions: [...state.subscriptions]..removeWhere((e) => event.urls.contains(e.actorId)), - urls: {...state.urls}..removeAll(event.urls), + subscriptions: [...state.subscriptions, ...newCommunities], + urls: {...state.urls}..addAll(newCommunities.map((e) => e.actorId)), + message: null, + errorReason: null, ), ); } catch (e) { @@ -47,19 +66,15 @@ class AnonymousSubscriptionsBloc extends Bloc _addSubscriptions(AddSubscriptionsEvent event, Emitter emit) async { + Future removeSubscriptions(Set urls) async { try { - // Filter out already subscribed communities - final communities = event.communities.where((ThunderCommunity community) => !state.urls.contains(community.actorId)).toList(); - if (communities.isEmpty) return; - - await insertSubscriptions(communities.toSet()); + await AnonymousSubscriptions.deleteCommunities(urls); emit( state.copyWith( status: AnonymousSubscriptionsStatus.success, - subscriptions: [...state.subscriptions, ...communities], - urls: {...state.urls}..addAll(communities.map((e) => e.actorId)), + subscriptions: [...state.subscriptions]..removeWhere((e) => urls.contains(e.actorId)), + urls: {...state.urls}..removeAll(urls), message: null, errorReason: null, ), @@ -74,7 +89,7 @@ class AnonymousSubscriptionsBloc extends Bloc _getSubscribedCommunities(GetSubscribedCommunitiesEvent event, Emitter emit) async { + Future _loadSubscribedCommunities() async { emit(state.copyWith( status: AnonymousSubscriptionsStatus.loading, message: null, @@ -82,15 +97,15 @@ class AnonymousSubscriptionsBloc extends Bloc subscribedCommunities = await getSubscriptions(); - - // Filter out duplicate communities based on URL - Map communities = {}; + final subscribedCommunities = await getSubscriptions(); + final communities = {}; for (final community in subscribedCommunities) { communities[community.actorId] = community; } + _lastLoadAt = DateTime.now(); + emit( state.copyWith( status: AnonymousSubscriptionsStatus.success, diff --git a/lib/src/features/community/presentation/state/anonymous_subscriptions_event.dart b/lib/src/features/community/presentation/state/anonymous_subscriptions_event.dart deleted file mode 100644 index efe86aa39..000000000 --- a/lib/src/features/community/presentation/state/anonymous_subscriptions_event.dart +++ /dev/null @@ -1,33 +0,0 @@ -part of 'anonymous_subscriptions_bloc.dart'; - -abstract class AnonymousSubscriptionsEvent extends Equatable { - const AnonymousSubscriptionsEvent(); - - @override - List get props => []; -} - -/// Gets the subscribed communities from the local subscriptions -class GetSubscribedCommunitiesEvent extends AnonymousSubscriptionsEvent {} - -/// Adds a given set of communities to the local subscriptions -class AddSubscriptionsEvent extends AnonymousSubscriptionsEvent { - /// The communities to add - final Set communities; - - const AddSubscriptionsEvent({required this.communities}); - - @override - List get props => [communities]; -} - -/// Deletes a given set of subscriptions by their actor ids -class DeleteSubscriptionsEvent extends AnonymousSubscriptionsEvent { - /// The actor ids of the communities to delete - final Set urls; - - const DeleteSubscriptionsEvent({required this.urls}); - - @override - List get props => [urls]; -} diff --git a/lib/src/features/community/presentation/state/anonymous_subscriptions_state.dart b/lib/src/features/community/presentation/state/anonymous_subscriptions_state.dart index d672501cb..3dac44d35 100644 --- a/lib/src/features/community/presentation/state/anonymous_subscriptions_state.dart +++ b/lib/src/features/community/presentation/state/anonymous_subscriptions_state.dart @@ -1,4 +1,4 @@ -part of 'anonymous_subscriptions_bloc.dart'; +part of 'anonymous_subscriptions_cubit.dart'; enum AnonymousSubscriptionsStatus { initial, loading, refreshing, success, empty, failure } @@ -13,19 +13,10 @@ class AnonymousSubscriptionsState extends Equatable { this.errorReason, }); - /// Status of the bloc final AnonymousSubscriptionsStatus status; - - /// Error message final String? message; - - /// Typed error reason for deterministic failure handling. final AppErrorReason? errorReason; - - /// List of subscribed communities final List subscriptions; - - /// Set of community actor ids (e.g., https://lemmy.ml/c/lemmy) final Set urls; AnonymousSubscriptionsState copyWith({ diff --git a/lib/src/features/community/presentation/widgets/community_drawer.dart b/lib/src/features/community/presentation/widgets/community_drawer.dart index 732f0326b..29c313635 100644 --- a/lib/src/features/community/presentation/widgets/community_drawer.dart +++ b/lib/src/features/community/presentation/widgets/community_drawer.dart @@ -34,6 +34,7 @@ class _CommunityDrawerState extends State { context.read().add(const FetchProfileSubscriptions()); context.read().add(const FetchProfileFavorites()); + context.read().loadSubscribedCommunities(); } @override @@ -46,8 +47,7 @@ class _CommunityDrawerState extends State { final feedCubit = context.read(); - AnonymousSubscriptionsBloc subscriptionsBloc = context.watch(); - subscriptionsBloc.add(GetSubscribedCommunitiesEvent()); + final subscriptionsCubit = context.watch(); bool isLoggedIn = context.watch().state.isLoggedIn; @@ -60,7 +60,7 @@ class _CommunityDrawerState extends State { subscriptions = filteredSubscriptions; } else { - subscriptions = subscriptionsBloc.state.subscriptions; + subscriptions = subscriptionsCubit.state.subscriptions; } return Drawer( diff --git a/lib/src/features/community/presentation/widgets/community_header/community_header.dart b/lib/src/features/community/presentation/widgets/community_header/community_header.dart index 304761529..a653ee3f9 100644 --- a/lib/src/features/community/presentation/widgets/community_header/community_header.dart +++ b/lib/src/features/community/presentation/widgets/community_header/community_header.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:thunder/src/features/community/community.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; @@ -50,18 +51,27 @@ class _CommunityHeaderState extends State { content = GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => showModalBottomSheet( - context: context, - showDragHandle: true, - enableDrag: true, - useSafeArea: true, - scrollControlDisabledMaxHeightRatio: 0.90, - builder: (context) => CommunityInformation( - community: widget.community, - instance: widget.instance, - moderators: widget.moderators, - ), - ), + onTap: () { + final account = resolveEffectiveAccount(context); + + showModalBottomSheet( + context: context, + showDragHandle: true, + enableDrag: true, + useSafeArea: true, + scrollControlDisabledMaxHeightRatio: 0.90, + builder: (_) => wrapWithCapturedAccountContext( + context, + CommunityInformation( + launchContext: context, + account: account, + community: widget.community, + instance: widget.instance, + moderators: widget.moderators, + ), + ), + ); + }, child: _CommunityHeaderWithBanner(community: widget.community, child: content), ); diff --git a/lib/src/features/community/presentation/widgets/community_header/community_header_actions.dart b/lib/src/features/community/presentation/widgets/community_header/community_header_actions.dart index 75d6da387..276f90a36 100644 --- a/lib/src/features/community/presentation/widgets/community_header/community_header_actions.dart +++ b/lib/src/features/community/presentation/widgets/community_header/community_header_actions.dart @@ -218,7 +218,7 @@ class _AnonymousSubscriptionChip extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = GlobalContext.l10n; - final subscriptions = context.watch().state.urls; + final subscriptions = context.watch().state.urls; final isSubscribed = subscriptions.contains(community.actorId); return ThunderActionChip( @@ -228,10 +228,10 @@ class _AnonymousSubscriptionChip extends StatelessWidget { HapticFeedback.mediumImpact(); if (isSubscribed) { - context.read().add(DeleteSubscriptionsEvent(urls: {community.actorId})); + context.read().removeSubscriptions({community.actorId}); showSnackbar(l10n.unsubscribed); } else { - context.read().add(AddSubscriptionsEvent(communities: {community})); + context.read().addSubscriptions({community}); showSnackbar(l10n.subscribed); } }, diff --git a/lib/src/features/community/presentation/widgets/community_information.dart b/lib/src/features/community/presentation/widgets/community_information.dart index ed2233073..063a50d72 100644 --- a/lib/src/features/community/presentation/widgets/community_information.dart +++ b/lib/src/features/community/presentation/widgets/community_information.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/feed/feed.dart'; @@ -16,6 +17,9 @@ import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.d /// A widget that displays information about a community. class CommunityInformation extends StatelessWidget { + final BuildContext launchContext; + final Account account; + /// The community to display in the sidebar final ThunderCommunity community; @@ -27,6 +31,8 @@ class CommunityInformation extends StatelessWidget { const CommunityInformation({ super.key, + required this.launchContext, + required this.account, required this.community, this.instance, required this.moderators, @@ -44,7 +50,7 @@ class CommunityInformation extends StatelessWidget { SidebarSectionHeader(value: l10n.information), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: CommonMarkdownBody(body: community.description ?? '', imageMaxWidth: MediaQuery.of(context).size.width), + child: CommonMarkdownBody(body: community.description ?? '', imageMaxWidth: MediaQuery.of(context).size.width, launchContext: launchContext), ), SidebarSectionHeader(value: l10n.stats), Padding( @@ -55,7 +61,7 @@ class CommunityInformation extends StatelessWidget { SidebarSectionHeader(value: l10n.moderator(2)), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: CommunityModeratorList(moderators: moderators), + child: CommunityModeratorList(launchContext: launchContext, account: account, moderators: moderators), ), ], SizedBox(height: MediaQuery.of(context).viewPadding.bottom + 32.0), @@ -132,10 +138,13 @@ class CommunityStatsList extends StatelessWidget { /// A widget that displays a list of moderators for a community. class CommunityModeratorList extends StatelessWidget { + final BuildContext launchContext; + final Account account; + /// The moderators of the community final List moderators; - const CommunityModeratorList({super.key, required this.moderators}); + const CommunityModeratorList({super.key, required this.launchContext, required this.account, required this.moderators}); @override Widget build(BuildContext context) { @@ -143,7 +152,7 @@ class CommunityModeratorList extends StatelessWidget { children: [ for (final moderator in moderators) InkWell( - onTap: () => navigateToFeedPage(context, feedType: FeedType.user, userId: moderator.id), + onTap: () => navigateToFeedPage(launchContext, account: account, feedType: FeedType.user, userId: moderator.id), borderRadius: BorderRadius.circular(50), child: Padding( padding: const EdgeInsets.all(8.0), diff --git a/lib/src/features/community/presentation/widgets/community_list_entry.dart b/lib/src/features/community/presentation/widgets/community_list_entry.dart index 289368787..cb30e24cc 100644 --- a/lib/src/features/community/presentation/widgets/community_list_entry.dart +++ b/lib/src/features/community/presentation/widgets/community_list_entry.dart @@ -50,10 +50,9 @@ class _CommunityListEntryState extends State { context.read().add(const FetchProfileSubscriptions()); } else { if (!subscribed) { - context.read().add(AddSubscriptionsEvent(communities: {widget.community})); - context.read().add(GetSubscribedCommunitiesEvent()); + context.read().addSubscriptions({widget.community}); } else { - context.read().add(DeleteSubscriptionsEvent(urls: {widget.community.actorId})); + context.read().removeSubscriptions({widget.community.actorId}); } } } @@ -70,7 +69,7 @@ class _CommunityListEntryState extends State { final subscriptions = context.select>((bloc) => bloc.state.subscriptions); community = subscriptions.firstWhereOrNull((c) => c.actorId == widget.community.actorId) ?? widget.community; } else { - final subscriptions = context.select>((bloc) => bloc.state.subscriptions); + final subscriptions = context.select>((cubit) => cubit.state.subscriptions); community = subscriptions.firstWhereOrNull((c) => c.actorId == widget.community.actorId) ?? widget.community; } diff --git a/lib/src/features/community/presentation/widgets/post_card.dart b/lib/src/features/community/presentation/widgets/post_card.dart index 588732c8a..667702ee8 100644 --- a/lib/src/features/community/presentation/widgets/post_card.dart +++ b/lib/src/features/community/presentation/widgets/post_card.dart @@ -51,6 +51,21 @@ class PostCard extends StatefulWidget { /// The callback function when the user taps on a post. final Function() onTap; + /// Optional feed type override for contexts without a FeedBloc. + final FeedType? feedType; + + /// Optional feed list type override for contexts without a FeedBloc. + final FeedListType? feedListType; + + /// Optional callback for replacing a post in the current list. + final void Function(ThunderPost post)? onPostUpdated; + + /// Optional callback for dismissing a hidden post from view. + final void Function(int postId)? onDismissHiddenPost; + + /// Optional callback for dismissing blocked content from view. + final void Function({int? userId, int? communityId})? onDismissBlocked; + const PostCard({ super.key, required this.post, @@ -64,6 +79,11 @@ class PostCard extends StatefulWidget { required this.indicateRead, required this.isLastTapped, this.disableSwiping = false, + this.feedType, + this.feedListType, + this.onPostUpdated, + this.onDismissHiddenPost, + this.onDismissBlocked, }); @override @@ -176,7 +196,8 @@ class _PostCardState extends State { rightSecondaryPostGesture: rightSecondaryPostGesture, disableSwiping: widget.disableSwiping, ); - final feedType = context.select((bloc) => bloc.state.feedType); + final hasFeedBloc = context.findAncestorWidgetOfExactType>() != null; + final feedType = widget.feedType ?? (hasFeedBloc ? context.select((bloc) => bloc.state.feedType) : null); final postIsCompact = useCompactView || (pinnedPostsUseCompactView && (widget.post.featuredLocal || (feedType == FeedType.community && widget.post.featuredCommunity))) || (linkPostsUseCompactView && widget.post.media.isNotEmpty && widget.post.media.first.mediaType == MediaType.link); @@ -185,6 +206,8 @@ class _PostCardState extends State { Widget child = postIsCompact ? PostCardViewCompact( post: widget.post, + feedType: feedType, + feedListType: widget.feedListType, creator: widget.post.creator!, community: widget.post.community!, indicateRead: widget.indicateRead, @@ -197,6 +220,8 @@ class _PostCardState extends State { ) : PostCardViewComfortable( post: widget.post, + feedType: feedType, + feedListType: widget.feedListType, hideThumbnails: hideThumbnails, hideNsfwPreviews: hideNsfwPreviews, markPostReadOnMediaView: markPostReadOnMediaView, @@ -213,6 +238,10 @@ class _PostCardState extends State { }, onVoteAction: widget.onVoteAction, onSaveAction: widget.onSaveAction, + onPostUpdated: widget.onPostUpdated, + onDismissHiddenPost: widget.onDismissHiddenPost, + onDismissBlocked: widget.onDismissBlocked, + onMarkPostRead: () => widget.onReadAction(true), ); // Wrap the post card in an InkWell to handle taps and long presses @@ -230,19 +259,37 @@ class _PostCardState extends State { return; } if (post != null) { - context.read().add(FeedItemUpdatedEvent(post: post)); + if (widget.onPostUpdated != null) { + widget.onPostUpdated!(post); + } else if (hasFeedBloc) { + context.read().add(FeedItemUpdatedEvent(post: post)); + } } if (postAction == PostAction.hide) { - context.read().add(FeedDismissHiddenPostEvent(postId: post!.id)); + if (widget.onDismissHiddenPost != null) { + widget.onDismissHiddenPost!(post!.id); + } else if (hasFeedBloc) { + context.read().add(FeedDismissHiddenPostEvent(postId: post!.id)); + } else { + FeedActionScope.maybeOf(context)?.dismissHiddenPost(post!.id); + } } if (userAction == UserAction.block) { - context.read().dismissBlocked(userId: post!.creator!.id); + if (widget.onDismissBlocked != null) { + widget.onDismissBlocked!(userId: post!.creator!.id); + } else { + FeedActionScope.maybeOf(context)?.dismissBlocked(userId: post!.creator!.id); + } } if (communityAction == CommunityAction.block) { - context.read().dismissBlocked(communityId: post!.community!.id); + if (widget.onDismissBlocked != null) { + widget.onDismissBlocked!(communityId: post!.community!.id); + } else { + FeedActionScope.maybeOf(context)?.dismissBlocked(communityId: post!.community!.id); + } } }, ), @@ -329,7 +376,7 @@ class PostCardActionBackground extends StatelessWidget { @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; - final tabletMode = context.select((bloc) => bloc.state.tabletMode); + final tabletMode = context.select((bloc) => bloc.state.tabletMode); final leftPrimaryPostGesture = context.select((cubit) => cubit.state.leftPrimaryPostGesture); final rightPrimaryPostGesture = context.select((cubit) => cubit.state.rightPrimaryPostGesture); diff --git a/lib/src/features/community/presentation/widgets/post_card_metadata.dart b/lib/src/features/community/presentation/widgets/post_card_metadata.dart index 97c389a72..bc6c10443 100644 --- a/lib/src/features/community/presentation/widgets/post_card_metadata.dart +++ b/lib/src/features/community/presentation/widgets/post_card_metadata.dart @@ -6,8 +6,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; +import 'package:thunder/src/features/account/data/cache/profile_site_info_cache.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/feed/feed.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/packages/ui/ui.dart' show ScalableText, ThunderIconLabel; @@ -79,7 +81,13 @@ class PostCardMetadata extends StatelessWidget { Widget build(BuildContext context) { final postCardMetadataItems = context .select>((cubit) => postCardViewType == ViewMode.compact ? cubit.state.compactPostCardMetadataItems : cubit.state.cardPostCardMetadataItems); - final showScores = context.select((ProfileBloc bloc) => bloc.state.siteResponse?.myUser?.localUserView.localUser.showScores) ?? true; + bool showScores = true; + + try { + showScores = context.select((ProfileBloc bloc) => bloc.state.siteResponse?.myUser?.localUserView.localUser.showScores) ?? true; + } catch (_) { + showScores = true; + } final dim = this.dim ?? false; final voteType = this.voteType ?? 0; @@ -466,7 +474,7 @@ class UrlPostCardMetaData extends StatelessWidget { } /// Contains metadata related to the language of a given post. This is used in the [PostCardMetadata] widget. -class LanguagePostCardMetaData extends StatelessWidget { +class LanguagePostCardMetaData extends StatefulWidget { /// The language to display in the metadata. If null, no language will be displayed. /// Pass `-1` to indicate that this widget is for demonstration purposes, and `English` will be displayed. final int? languageId; @@ -474,21 +482,73 @@ class LanguagePostCardMetaData extends StatelessWidget { /// Whether or not the post has been read. This is used to determine the color. final bool hasBeenRead; + /// Optional explicit account used to resolve languages in overlays. + final Account? account; + const LanguagePostCardMetaData({ super.key, this.languageId, this.hasBeenRead = false, + this.account, }); + @override + State createState() => _LanguagePostCardMetaDataState(); +} + +class _LanguagePostCardMetaDataState extends State { + List _languages = const []; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _loadLanguagesIfNeeded(); + } + + Future _loadLanguagesIfNeeded() async { + if (widget.languageId == null || widget.languageId == -1) return; + + if (widget.account == null) { + try { + final languages = context.read().state.siteResponse?.allLanguages ?? const []; + if (!mounted || languages.isEmpty && _languages.isEmpty) return; + + if (_languages != languages) { + setState(() => _languages = languages); + } + return; + } catch (_) { + // Fall back to account-scoped site info when no ProfileBloc is available. + } + } + + final account = widget.account ?? resolveEffectiveAccount(context); + if (account.anonymous) return; + + final siteInfo = await ProfileSiteInfoCache.instance.get(account); + if (!mounted) return; + + setState(() => _languages = siteInfo.allLanguages ?? const []); + } + @override Widget build(BuildContext context) { String? languageName; - if (languageId == -1) { + if (widget.languageId == -1) { languageName = 'English'; - } else if (languageId != null) { - final languages = context.select((ProfileBloc bloc) => bloc.state.siteResponse?.allLanguages ?? []); - final language = languages.firstWhereOrNull((language) => language.id == languageId); + } else if (widget.languageId != null) { + List languages = _languages; + + if (widget.account == null) { + try { + languages = context.select((ProfileBloc bloc) => bloc.state.siteResponse?.allLanguages ?? []); + } catch (_) { + languages = _languages; + } + } + + final language = languages.firstWhereOrNull((language) => language.id == widget.languageId); languageName = language?.name; } @@ -498,7 +558,7 @@ class LanguagePostCardMetaData extends StatelessWidget { final fontScale = context.select((cubit) => cubit.state.metadataFontSizeScale); final readColor = theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.45); - final color = hasBeenRead ? readColor : theme.textTheme.bodyMedium?.color; + final color = widget.hasBeenRead ? readColor : theme.textTheme.bodyMedium?.color; return Container( margin: const EdgeInsets.only(right: 8.0), @@ -539,7 +599,7 @@ class CrossPostMetaData extends StatelessWidget { } class PostCommunityAndAuthor extends StatelessWidget { - const PostCommunityAndAuthor({super.key, required this.user, required this.community, this.dim}); + const PostCommunityAndAuthor({super.key, required this.user, required this.community, this.dim, this.feedType, this.feedListType}); /// The user to display in the metadata final ThunderUser user; @@ -550,21 +610,28 @@ class PostCommunityAndAuthor extends StatelessWidget { /// Whether or not to dim the color of the text and icons. This is usually used to indicate that the post has been read. final bool? dim; + /// Optional feed type override for contexts without a FeedBloc. + final FeedType? feedType; + + /// Optional feed list type override for contexts without a FeedBloc. + final FeedListType? feedListType; + @override Widget build(BuildContext context) { final showPostAuthor = context.select((cubit) => cubit.state.showPostAuthor); final showCommunityIcons = context.select((cubit) => cubit.state.showCommunityIcons); - final feedType = context.select((FeedBloc bloc) => bloc.state.feedType); - final showUsername = (showPostAuthor || feedType == FeedType.community) && feedType != FeedType.user; - final showCommunityName = feedType != FeedType.community; + final hasFeedBloc = context.findAncestorWidgetOfExactType>() != null; + final resolvedFeedType = feedType ?? (hasFeedBloc ? context.select((bloc) => bloc.state.feedType) : null); + final showUsername = (showPostAuthor || resolvedFeedType == FeedType.community) && resolvedFeedType != FeedType.user; + final showCommunityName = resolvedFeedType != FeedType.community; final dim = this.dim ?? false; return Row( spacing: 6.0, children: [ - if (showCommunityIcons && feedType != FeedType.community) + if (showCommunityIcons && resolvedFeedType != FeedType.community) GestureDetector( child: CommunityAvatar(community: community, radius: showUsername && showCommunityName ? 14 : 7, thumbnailSize: 50, format: 'png'), onTap: () => navigateToFeedPage(context, communityId: community.id, feedType: FeedType.community), @@ -579,6 +646,7 @@ class PostCommunityAndAuthor extends StatelessWidget { actorId: community.actorId, subscribed: community.subscribed != SubscriptionStatus.notSubscribed, dim: dim, + feedListType: feedListType, ), UserPostCardMetadata( username: user.name, @@ -595,6 +663,7 @@ class PostCommunityAndAuthor extends StatelessWidget { actorId: community.actorId, subscribed: community.subscribed != SubscriptionStatus.notSubscribed, dim: dim, + feedListType: feedListType, ) else if (showUsername) UserPostCardMetadata( @@ -616,6 +685,7 @@ class CommunityPostCardMetadata extends StatelessWidget { this.actorId, required this.dim, required this.subscribed, + this.feedListType, }); /// The name of the community @@ -633,6 +703,9 @@ class CommunityPostCardMetadata extends StatelessWidget { /// Whether the user is subscribed to the community final bool subscribed; + /// Optional feed list type override for contexts without a FeedBloc. + final FeedListType? feedListType; + Color? _transformColor(Color? color) => dim ? color?.withValues(alpha: 0.45) : color?.withValues(alpha: 0.75); @override @@ -640,10 +713,11 @@ class CommunityPostCardMetadata extends StatelessWidget { final theme = Theme.of(context); final fontScale = context.select((cubit) => cubit.state.metadataFontSizeScale); - final feedListType = context.select((FeedBloc bloc) => bloc.state.feedListType); + final hasFeedBloc = context.findAncestorWidgetOfExactType>() != null; + final resolvedFeedListType = feedListType ?? (hasFeedBloc ? context.select((bloc) => bloc.state.feedListType) : null); final instanceName = actorId != null ? fetchInstanceNameFromUrl(actorId) : null; - final showCommunitySubscription = (feedListType == FeedListType.all || feedListType == FeedListType.local) && subscribed; + final showCommunitySubscription = (resolvedFeedListType == FeedListType.all || resolvedFeedListType == FeedListType.local) && subscribed; Widget child = CommunityFullNameWidget(name: communityName, displayName: displayName, instance: instanceName, fontScale: fontScale, transformColor: _transformColor); diff --git a/lib/src/features/community/presentation/widgets/post_card_view_comfortable.dart b/lib/src/features/community/presentation/widgets/post_card_view_comfortable.dart index d2e52d095..0a22d20e2 100644 --- a/lib/src/features/community/presentation/widgets/post_card_view_comfortable.dart +++ b/lib/src/features/community/presentation/widgets/post_card_view_comfortable.dart @@ -19,6 +19,12 @@ class PostCardViewComfortable extends StatelessWidget { /// The post to display. final ThunderPost post; + /// Optional feed type override for contexts without a FeedBloc. + final FeedType? feedType; + + /// Optional feed list type override for contexts without a FeedBloc. + final FeedListType? feedListType; + /// Whether to hide thumbnails. final bool hideThumbnails; @@ -58,9 +64,23 @@ class PostCardViewComfortable extends StatelessWidget { /// The function to handle save actions. final Function(bool) onSaveAction; + /// Optional callback for replacing a post in the current list. + final void Function(ThunderPost post)? onPostUpdated; + + /// Optional callback for dismissing a hidden post from view. + final void Function(int postId)? onDismissHiddenPost; + + /// Optional callback for dismissing blocked content from view. + final void Function({int? userId, int? communityId})? onDismissBlocked; + + /// Optional callback for marking a post as read from media interactions. + final Future Function()? onMarkPostRead; + const PostCardViewComfortable({ super.key, required this.post, + this.feedType, + this.feedListType, required this.hideThumbnails, required this.hideNsfwPreviews, required this.edgeToEdgeImages, @@ -74,6 +94,10 @@ class PostCardViewComfortable extends StatelessWidget { this.navigateToPost, required this.onVoteAction, required this.onSaveAction, + this.onPostUpdated, + this.onDismissHiddenPost, + this.onDismissBlocked, + this.onMarkPostRead, }); /// Returns the color of the container based on the current theme and whether the post is dimmed or not. @@ -91,17 +115,28 @@ class PostCardViewComfortable extends StatelessWidget { void onPostActionBottomSheetPressed(BuildContext context, ThunderPost post) { HapticFeedback.mediumImpact(); + final hasFeedBloc = context.findAncestorWidgetOfExactType>() != null; showPostActionBottomModalSheet( context, post, onAction: ({postAction, userAction, communityAction, post}) { if (postAction == null && userAction == null && communityAction == null) return; - if (post != null) context.read().add(FeedItemUpdatedEvent(post: post)); + if (post != null) { + if (onPostUpdated != null) { + onPostUpdated!(post); + } else if (hasFeedBloc) { + context.read().add(FeedItemUpdatedEvent(post: post)); + } + } switch (postAction) { case PostAction.hide: - context.read().dismissHiddenPost(post!.id); + if (onDismissHiddenPost != null) { + onDismissHiddenPost!(post!.id); + } else { + FeedActionScope.maybeOf(context)?.dismissHiddenPost(post!.id); + } break; default: break; @@ -109,7 +144,11 @@ class PostCardViewComfortable extends StatelessWidget { switch (userAction) { case UserAction.block: - context.read().dismissBlocked(userId: post!.creator!.id); + if (onDismissBlocked != null) { + onDismissBlocked!(userId: post!.creator!.id); + } else { + FeedActionScope.maybeOf(context)?.dismissBlocked(userId: post!.creator!.id); + } break; default: break; @@ -117,7 +156,11 @@ class PostCardViewComfortable extends StatelessWidget { switch (communityAction) { case CommunityAction.block: - context.read().dismissBlocked(communityId: post!.community!.id); + if (onDismissBlocked != null) { + onDismissBlocked!(communityId: post!.community!.id); + } else { + FeedActionScope.maybeOf(context)?.dismissBlocked(communityId: post!.community!.id); + } break; default: break; @@ -164,6 +207,7 @@ class PostCardViewComfortable extends StatelessWidget { isUserLoggedIn: isUserLoggedIn, navigateToPost: navigateToPost, read: dim, + onMarkPostRead: onMarkPostRead, ); } @@ -171,6 +215,8 @@ class PostCardViewComfortable extends StatelessWidget { user: post.creator!, community: post.community!, dim: dim, + feedType: feedType, + feedListType: feedListType, ); final edgesPadding = const EdgeInsets.symmetric(horizontal: 12.0); diff --git a/lib/src/features/community/presentation/widgets/post_card_view_compact.dart b/lib/src/features/community/presentation/widgets/post_card_view_compact.dart index fa01dd259..ef06e78b7 100644 --- a/lib/src/features/community/presentation/widgets/post_card_view_compact.dart +++ b/lib/src/features/community/presentation/widgets/post_card_view_compact.dart @@ -26,6 +26,12 @@ class PostCardViewCompact extends StatelessWidget { /// Determines whether the post should be dimmed or not. This is usually to indicate when a post has been read. final bool? indicateRead; + /// Optional feed type override for contexts without a FeedBloc. + final FeedType? feedType; + + /// Optional feed list type override for contexts without a FeedBloc. + final FeedListType? feedListType; + /// Determines whether the media thumbnails should be shown or not. final bool showMedia; @@ -39,6 +45,8 @@ class PostCardViewCompact extends StatelessWidget { required this.community, this.navigateToPost, this.indicateRead, + this.feedType, + this.feedListType, this.showMedia = true, required this.isLastTapped, }); @@ -81,7 +89,7 @@ class PostCardViewCompact extends StatelessWidget { final edited = post.updated != null; final mediaUrl = post.media.firstOrNull?.originalUrl; - final postCardAuthor = PostCommunityAndAuthor(user: creator, community: community, dim: dim); + final postCardAuthor = PostCommunityAndAuthor(user: creator, community: community, dim: dim, feedType: feedType, feedListType: feedListType); return Container( color: containerColor, diff --git a/lib/src/features/feed/application/state/feed_ui_cubit.dart b/lib/src/features/feed/application/state/feed_ui_cubit.dart deleted file mode 100644 index 090e072af..000000000 --- a/lib/src/features/feed/application/state/feed_ui_cubit.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; - -part 'feed_ui_state.dart'; - -/// Cubit for managing feed's state for certain actions -class FeedUiCubit extends Cubit { - FeedUiCubit() : super(const FeedUiState()); - - /// Increments the scrollId. This is used to trigger a scroll to top action - void scrollToTop() { - emit(state.copyWith(scrollId: state.scrollId + 1)); - } - - /// Increments the dismissReadId. This is used to trigger dismissing read posts action - void dismissRead() { - emit(state.copyWith(dismissReadId: state.dismissReadId + 1)); - } - - /// Sets the dismiss blocked user/community IDs. This is used to trigger dismissing blocked user/community posts action - void dismissBlocked({int? userId, int? communityId}) { - emit(state.copyWith( - dismissBlockedUserId: userId, - dismissBlockedCommunityId: communityId, - )); - } - - /// Sets the dismiss hidden post ID. This is used to trigger dismissing hidden posts action - void dismissHiddenPost(int postId) { - emit(state.copyWith(dismissHiddenPostId: postId)); - } - - /// Clears the dismiss hidden post ID - void clearDismissHiddenPost() { - emit(state.copyWith(dismissHiddenPostId: null)); - } - - /// Clears the dismiss blocked user/community IDs - void clearDismissBlocked() { - emit(state.copyWith( - dismissBlockedUserId: null, - dismissBlockedCommunityId: null, - )); - } -} diff --git a/lib/src/features/feed/application/state/feed_ui_state.dart b/lib/src/features/feed/application/state/feed_ui_state.dart deleted file mode 100644 index eae3c2301..000000000 --- a/lib/src/features/feed/application/state/feed_ui_state.dart +++ /dev/null @@ -1,53 +0,0 @@ -part of 'feed_ui_cubit.dart'; - -const _feedUiUnset = Object(); - -class FeedUiState extends Equatable { - const FeedUiState({ - this.scrollId = 0, - this.dismissReadId = 0, - this.dismissBlockedUserId, - this.dismissBlockedCommunityId, - this.dismissHiddenPostId, - }); - - /// This id is used for scrolling back to the top - final int scrollId; - - /// This id is used for dismissing already read posts in the feed - final int dismissReadId; - - /// This id is used for dismissing posts from blocked users - final int? dismissBlockedUserId; - - /// This id is used for dismissing posts from blocked communities - final int? dismissBlockedCommunityId; - - /// This id is used for dismissing posts that have been hidden by the user - final int? dismissHiddenPostId; - - FeedUiState copyWith({ - int? scrollId, - int? dismissReadId, - Object? dismissBlockedUserId = _feedUiUnset, - Object? dismissBlockedCommunityId = _feedUiUnset, - Object? dismissHiddenPostId = _feedUiUnset, - }) { - return FeedUiState( - scrollId: scrollId ?? this.scrollId, - dismissReadId: dismissReadId ?? this.dismissReadId, - dismissBlockedUserId: identical(dismissBlockedUserId, _feedUiUnset) ? this.dismissBlockedUserId : dismissBlockedUserId as int?, - dismissBlockedCommunityId: identical(dismissBlockedCommunityId, _feedUiUnset) ? this.dismissBlockedCommunityId : dismissBlockedCommunityId as int?, - dismissHiddenPostId: identical(dismissHiddenPostId, _feedUiUnset) ? this.dismissHiddenPostId : dismissHiddenPostId as int?, - ); - } - - @override - List get props => [ - scrollId, - dismissReadId, - dismissBlockedUserId, - dismissBlockedCommunityId, - dismissHiddenPostId, - ]; -} diff --git a/lib/src/features/feed/application/state/nav_bar_state_cubit/nav_bar_state.dart b/lib/src/features/feed/application/state/nav_bar_state_cubit/nav_bar_state.dart deleted file mode 100644 index bbf330287..000000000 --- a/lib/src/features/feed/application/state/nav_bar_state_cubit/nav_bar_state.dart +++ /dev/null @@ -1,21 +0,0 @@ -part of 'nav_bar_state_cubit.dart'; - -class NavBarState extends Equatable { - const NavBarState({ - this.isBottomNavBarVisible = true, - }); - - /// Whether the bottom navigation bar is currently visible - final bool isBottomNavBarVisible; - - NavBarState copyWith({ - bool? isBottomNavBarVisible, - }) { - return NavBarState( - isBottomNavBarVisible: isBottomNavBarVisible ?? this.isBottomNavBarVisible, - ); - } - - @override - List get props => [isBottomNavBarVisible]; -} diff --git a/lib/src/features/feed/application/state/nav_bar_state_cubit/nav_bar_state_cubit.dart b/lib/src/features/feed/application/state/nav_bar_state_cubit/nav_bar_state_cubit.dart deleted file mode 100644 index e35f61326..000000000 --- a/lib/src/features/feed/application/state/nav_bar_state_cubit/nav_bar_state_cubit.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; - -part 'nav_bar_state.dart'; - -/// Cubit for managing bottom navigation bar state -class NavBarStateCubit extends Cubit { - NavBarStateCubit() : super(const NavBarState()); - - /// Sets the bottom navigation bar visibility - void setBottomNavBarVisible(bool isVisible) { - emit(state.copyWith(isBottomNavBarVisible: isVisible)); - } -} diff --git a/lib/src/features/feed/domain/enums/fab_action.dart b/lib/src/features/feed/domain/enums/fab_action.dart index f353a936e..e20da1bea 100644 --- a/lib/src/features/feed/domain/enums/fab_action.dart +++ b/lib/src/features/feed/domain/enums/fab_action.dart @@ -1,124 +1,124 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:thunder/l10n/generated/app_localizations.dart'; - -import 'package:thunder/src/features/post/api.dart'; -import 'package:thunder/src/features/feed/api.dart'; -import 'package:thunder/src/foundation/config/global_context.dart'; - -enum FeedFabAction { - openFab(), - backToTop(), - subscriptions(), - changeSort(), - refresh(), - dismissRead(), - newPost(); - - IconData get icon { - switch (this) { - case FeedFabAction.openFab: - return Icons.more_horiz_rounded; - case FeedFabAction.backToTop: - return Icons.arrow_upward; - case FeedFabAction.subscriptions: - return Icons.people_rounded; - case FeedFabAction.changeSort: - return Icons.sort_rounded; - case FeedFabAction.refresh: - return Icons.refresh_rounded; - case FeedFabAction.dismissRead: - return Icons.clear_all_rounded; - case FeedFabAction.newPost: - return Icons.add_rounded; - } - } - - String get title { - switch (this) { - case FeedFabAction.openFab: - return AppLocalizations.of(GlobalContext.context)!.open; - case FeedFabAction.backToTop: - return AppLocalizations.of(GlobalContext.context)!.backToTop; - case FeedFabAction.subscriptions: - return AppLocalizations.of(GlobalContext.context)!.subscriptions; - case FeedFabAction.changeSort: - return AppLocalizations.of(GlobalContext.context)!.changeSort; - case FeedFabAction.refresh: - return AppLocalizations.of(GlobalContext.context)!.refresh; - case FeedFabAction.dismissRead: - return AppLocalizations.of(GlobalContext.context)!.dismissRead; - case FeedFabAction.newPost: - return AppLocalizations.of(GlobalContext.context)!.createPost; - } - } -} - -enum PostFabAction { - openFab(), - backToTop(), - changeSort(), - replyToPost(), - refresh(), - search(); - - IconData getIcon({IconData? override, bool postLocked = false}) { - if (override != null) { - return override; - } - - switch (this) { - case PostFabAction.openFab: - return Icons.more_horiz_rounded; - case PostFabAction.backToTop: - return Icons.arrow_upward; - case PostFabAction.changeSort: - return Icons.sort_rounded; - case PostFabAction.replyToPost: - if (postLocked) { - return Icons.lock; - } - return Icons.reply_rounded; - case PostFabAction.refresh: - return Icons.refresh_rounded; - case PostFabAction.search: - return Icons.search_rounded; - } - } - - String getTitle(BuildContext context, {bool postLocked = false}) { - switch (this) { - case PostFabAction.openFab: - return AppLocalizations.of(context)!.open; - case PostFabAction.backToTop: - return AppLocalizations.of(context)!.backToTop; - case PostFabAction.changeSort: - return AppLocalizations.of(context)!.changeSort; - case PostFabAction.replyToPost: - if (postLocked) { - return AppLocalizations.of(context)!.postLocked; - } - return AppLocalizations.of(context)!.replyToPost; - case PostFabAction.refresh: - return AppLocalizations.of(context)!.refresh; - case PostFabAction.search: - return AppLocalizations.of(context)!.search; - } - } - - void execute({BuildContext? context, void Function()? override, ThunderPost? post, int? postId, int? highlightedCommentId, String? selectedCommentPath}) { - if (override != null) { - override(); - return; - } - - switch (this) { - case PostFabAction.openFab: - context?.read().setPostFabOpen(true); - case PostFabAction.refresh: - context?.read().add(GetPostEvent(post: post, postId: postId, selectedCommentPath: selectedCommentPath)); - default: - break; - } - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thunder/l10n/generated/app_localizations.dart'; +import 'package:thunder/src/app/shell/state/shell_chrome_cubit.dart'; + +import 'package:thunder/src/features/post/api.dart'; +import 'package:thunder/src/foundation/config/global_context.dart'; + +enum FeedFabAction { + openFab(), + backToTop(), + subscriptions(), + changeSort(), + refresh(), + dismissRead(), + newPost(); + + IconData get icon { + switch (this) { + case FeedFabAction.openFab: + return Icons.more_horiz_rounded; + case FeedFabAction.backToTop: + return Icons.arrow_upward; + case FeedFabAction.subscriptions: + return Icons.people_rounded; + case FeedFabAction.changeSort: + return Icons.sort_rounded; + case FeedFabAction.refresh: + return Icons.refresh_rounded; + case FeedFabAction.dismissRead: + return Icons.clear_all_rounded; + case FeedFabAction.newPost: + return Icons.add_rounded; + } + } + + String get title { + switch (this) { + case FeedFabAction.openFab: + return AppLocalizations.of(GlobalContext.context)!.open; + case FeedFabAction.backToTop: + return AppLocalizations.of(GlobalContext.context)!.backToTop; + case FeedFabAction.subscriptions: + return AppLocalizations.of(GlobalContext.context)!.subscriptions; + case FeedFabAction.changeSort: + return AppLocalizations.of(GlobalContext.context)!.changeSort; + case FeedFabAction.refresh: + return AppLocalizations.of(GlobalContext.context)!.refresh; + case FeedFabAction.dismissRead: + return AppLocalizations.of(GlobalContext.context)!.dismissRead; + case FeedFabAction.newPost: + return AppLocalizations.of(GlobalContext.context)!.createPost; + } + } +} + +enum PostFabAction { + openFab(), + backToTop(), + changeSort(), + replyToPost(), + refresh(), + search(); + + IconData getIcon({IconData? override, bool postLocked = false}) { + if (override != null) { + return override; + } + + switch (this) { + case PostFabAction.openFab: + return Icons.more_horiz_rounded; + case PostFabAction.backToTop: + return Icons.arrow_upward; + case PostFabAction.changeSort: + return Icons.sort_rounded; + case PostFabAction.replyToPost: + if (postLocked) { + return Icons.lock; + } + return Icons.reply_rounded; + case PostFabAction.refresh: + return Icons.refresh_rounded; + case PostFabAction.search: + return Icons.search_rounded; + } + } + + String getTitle(BuildContext context, {bool postLocked = false}) { + switch (this) { + case PostFabAction.openFab: + return AppLocalizations.of(context)!.open; + case PostFabAction.backToTop: + return AppLocalizations.of(context)!.backToTop; + case PostFabAction.changeSort: + return AppLocalizations.of(context)!.changeSort; + case PostFabAction.replyToPost: + if (postLocked) { + return AppLocalizations.of(context)!.postLocked; + } + return AppLocalizations.of(context)!.replyToPost; + case PostFabAction.refresh: + return AppLocalizations.of(context)!.refresh; + case PostFabAction.search: + return AppLocalizations.of(context)!.search; + } + } + + void execute({BuildContext? context, void Function()? override, ThunderPost? post, int? postId, int? highlightedCommentId, String? selectedCommentPath}) { + if (override != null) { + override(); + return; + } + + switch (this) { + case PostFabAction.openFab: + context?.read().setPostFabOpen(true); + case PostFabAction.refresh: + context?.read().add(GetPostEvent(post: post, postId: postId, selectedCommentPath: selectedCommentPath)); + default: + break; + } + } +} diff --git a/lib/src/features/feed/feed.dart b/lib/src/features/feed/feed.dart index 904d5b904..5905d8301 100644 --- a/lib/src/features/feed/feed.dart +++ b/lib/src/features/feed/feed.dart @@ -1,9 +1,7 @@ export 'presentation/state/feed_bloc.dart'; export 'application/state/fab_preferences_cubit.dart'; -export 'application/state/fab_state_cubit.dart'; export 'application/state/feed_preferences_cubit.dart'; -export 'application/state/feed_ui_cubit.dart'; -export 'application/state/nav_bar_state_cubit/nav_bar_state_cubit.dart'; +export 'presentation/controllers/feed_action_controller.dart'; export 'domain/enums/enums.dart'; export 'domain/models/feed_result.dart'; export 'presentation/pages/pages.dart'; diff --git a/lib/src/features/feed/presentation/controllers/feed_action_controller.dart b/lib/src/features/feed/presentation/controllers/feed_action_controller.dart new file mode 100644 index 000000000..70e77e880 --- /dev/null +++ b/lib/src/features/feed/presentation/controllers/feed_action_controller.dart @@ -0,0 +1,64 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +typedef FeedScrollToTopCallback = Future Function(); +typedef FeedDismissReadCallback = Future Function(); +typedef FeedDismissBlockedCallback = Future Function({int? userId, int? communityId}); +typedef FeedDismissHiddenPostCallback = Future Function(int postId); + +class FeedActionController { + FeedScrollToTopCallback? _scrollToTop; + FeedDismissReadCallback? _dismissRead; + FeedDismissBlockedCallback? _dismissBlocked; + FeedDismissHiddenPostCallback? _dismissHiddenPost; + + Object? _bindingToken; + + void bind({ + required Object token, + required FeedScrollToTopCallback scrollToTop, + required FeedDismissReadCallback dismissRead, + required FeedDismissBlockedCallback dismissBlocked, + required FeedDismissHiddenPostCallback dismissHiddenPost, + }) { + _bindingToken = token; + _scrollToTop = scrollToTop; + _dismissRead = dismissRead; + _dismissBlocked = dismissBlocked; + _dismissHiddenPost = dismissHiddenPost; + } + + void unbind(Object token) { + if (_bindingToken != token) { + return; + } + + _bindingToken = null; + _scrollToTop = null; + _dismissRead = null; + _dismissBlocked = null; + _dismissHiddenPost = null; + } + + Future scrollToTop() async => _scrollToTop?.call(); + + Future dismissRead() async => _dismissRead?.call(); + + Future dismissBlocked({int? userId, int? communityId}) async => _dismissBlocked?.call(userId: userId, communityId: communityId); + + Future dismissHiddenPost(int postId) async => _dismissHiddenPost?.call(postId); +} + +class FeedActionScope extends InheritedWidget { + const FeedActionScope({super.key, required this.controller, required super.child}); + + final FeedActionController controller; + + static FeedActionController? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType()?.controller; + } + + @override + bool updateShouldNotify(FeedActionScope oldWidget) => controller != oldWidget.controller; +} diff --git a/lib/src/features/feed/presentation/pages/feed_page.dart b/lib/src/features/feed/presentation/pages/feed_page.dart index 45affe860..ae437ced0 100644 --- a/lib/src/features/feed/presentation/pages/feed_page.dart +++ b/lib/src/features/feed/presentation/pages/feed_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thunder/src/app/shell/state/shell_chrome_cubit.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/app/wiring/state_factories.dart'; @@ -13,6 +14,7 @@ import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; @@ -36,6 +38,8 @@ enum FeedType { community, user, general, account } class FeedPage extends StatefulWidget { const FeedPage({ super.key, + this.account, + this.actionController, this.useGlobalFeedBloc = false, required this.feedType, this.feedListType, @@ -52,6 +56,12 @@ class FeedPage extends StatefulWidget { /// The type of feed to display. final FeedType feedType; + /// Optional explicit account for route-scoped feeds. + final Account? account; + + /// Optional controller for page-scoped feed actions. + final FeedActionController? actionController; + /// The type of general feed to display: all, local, subscribed. final FeedListType? feedListType; @@ -90,6 +100,8 @@ class FeedPage extends StatefulWidget { } class _FeedPageState extends State with AutomaticKeepAliveClientMixin { + late final FeedActionController _actionController = widget.actionController ?? FeedActionController(); + @override bool get wantKeepAlive => true; @@ -129,15 +141,19 @@ class _FeedPageState extends State with AutomaticKeepAliveClientMixin< return BlocProvider.value( value: bloc, - child: FeedView( - scaffoldStateKey: widget.scaffoldStateKey, - feedType: widget.feedType, - isActive: widget.isActive, + child: FeedActionScope( + controller: _actionController, + child: FeedView( + actionController: _actionController, + scaffoldStateKey: widget.scaffoldStateKey, + feedType: widget.feedType, + isActive: widget.isActive, + ), ), ); } - final account = context.select((bloc) => bloc.state.account); + final account = widget.account ?? resolveEffectiveAccount(context); return BlocProvider( create: (_) => createFeedBloc(account) @@ -152,17 +168,23 @@ class _FeedPageState extends State with AutomaticKeepAliveClientMixin< reset: true, showHidden: widget.showHidden, )), - child: FeedView( - scaffoldStateKey: widget.scaffoldStateKey, - feedType: widget.feedType, - isActive: widget.isActive, + child: FeedActionScope( + controller: _actionController, + child: FeedView( + actionController: _actionController, + scaffoldStateKey: widget.scaffoldStateKey, + feedType: widget.feedType, + isActive: widget.isActive, + ), ), ); } } class FeedView extends StatefulWidget { - const FeedView({super.key, this.scaffoldStateKey, this.feedType, this.isActive = false}); + const FeedView({super.key, required this.actionController, this.scaffoldStateKey, this.feedType, this.isActive = false}); + + final FeedActionController actionController; /// The scaffold key which holds the drawer final GlobalKey? scaffoldStateKey; @@ -179,6 +201,7 @@ class FeedView extends StatefulWidget { class _FeedViewState extends State { final ScrollController _scrollController = ScrollController(); + final Object _actionBindingToken = Object(); /// Indicates which "tab" is selected. This is used for user profiles, where we can switch between posts and comments List selectedUserOption = [true, false]; @@ -201,10 +224,47 @@ class _FeedViewState extends State { void initState() { super.initState(); + _bindActionController(); _scrollController.addListener(_onScroll); BackButtonInterceptor.add(_handleBack); } + @override + void didUpdateWidget(covariant FeedView oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.actionController != widget.actionController) { + oldWidget.actionController.unbind(_actionBindingToken); + _bindActionController(); + } + } + + void _bindActionController() { + widget.actionController.bind( + token: _actionBindingToken, + scrollToTop: _scrollToTop, + dismissRead: dismissRead, + dismissBlocked: ({int? userId, int? communityId}) => dismissBlockedUsersAndCommunities(userId, communityId), + dismissHiddenPost: _dismissHiddenPostFromScope, + ); + } + + Future _scrollToTop() async { + if (!_scrollController.hasClients) { + return; + } + + await _scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); + } + + Future _dismissHiddenPostFromScope(int postId) async { + if (context.read().state.showHiddenPosts) { + return; + } + + await dismissHiddenPost(postId); + } + void _onScroll() { // Fetches new posts when the user has scrolled past 70% list if (_scrollController.position.pixels > _scrollController.position.maxScrollExtent * 0.7 && context.read().state.status != FeedStatus.fetching) { @@ -216,18 +276,18 @@ class _FeedViewState extends State { final delta = currentScrollPosition - _previousScrollPosition; if (delta.abs() > _scrollThreshold) { - _cachedHideBottomBarOnScroll ??= context.read().state.hideBottomBarOnScroll; // Still in ThunderBloc as it's a global setting + _cachedHideBottomBarOnScroll ??= context.read().state.hideBottomBarOnScroll; // Still in ThunderCubit as it's a global setting if (_cachedHideBottomBarOnScroll == true) { final isScrollingDown = delta > 0; - final isBottomNavBarVisible = context.read().state.isBottomNavBarVisible; + final isBottomNavBarVisible = context.read().state.isBottomNavBarVisible; // Only dispatch if the visibility state needs to change // Show nav bar when scrolling up, hide when scrolling down if (isScrollingDown && isBottomNavBarVisible) { - context.read().setBottomNavBarVisible(false); + context.read().setBottomNavBarVisible(false); } else if (!isScrollingDown && !isBottomNavBarVisible) { - context.read().setBottomNavBarVisible(true); + context.read().setBottomNavBarVisible(true); } } _previousScrollPosition = currentScrollPosition; @@ -236,6 +296,7 @@ class _FeedViewState extends State { @override void dispose() { + widget.actionController.unbind(_actionBindingToken); _scrollController.removeListener(_onScroll); _scrollController.dispose(); BackButtonInterceptor.remove(_handleBack); @@ -313,210 +374,191 @@ class _FeedViewState extends State { Widget build(BuildContext context) { final l10n = GlobalContext.l10n; - final tabletMode = context.select((bloc) => bloc.state.tabletMode); + final tabletMode = context.select((bloc) => bloc.state.tabletMode); final markPostReadOnScroll = context.select((cubit) => cubit.state.markPostReadOnScroll); - final hideTopBarOnScroll = context.select((bloc) => bloc.state.hideTopBarOnScroll); - final isFabOpen = context.select((cubit) => cubit.state.isFeedFabOpen); + final hideTopBarOnScroll = context.select((bloc) => bloc.state.hideTopBarOnScroll); + final isFabOpen = context.select((cubit) => cubit.state.isFeedFabOpen); final enableFeedsFab = context.select((cubit) => cubit.state.enableFeedsFab); - final showHiddenPosts = context.select((cubit) => cubit.state.showHiddenPosts); - return BlocListener( - listenWhen: (previous, current) { - if (previous.scrollId != current.scrollId) { - _scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); - } - if (previous.dismissReadId != current.dismissReadId) { - dismissRead(); - } - if (current.dismissBlockedUserId != null || current.dismissBlockedCommunityId != null) { - dismissBlockedUsersAndCommunities(current.dismissBlockedUserId, current.dismissBlockedCommunityId); - } - if (current.dismissHiddenPostId != null && !showHiddenPosts) { - dismissHiddenPost(current.dismissHiddenPostId!); - } - return true; - }, - listener: (context, state) {}, - child: Scaffold( - body: SafeArea( - top: false, - child: BlocConsumer( - listenWhen: (previous, current) { - if (current.excessiveApiCalls) { - showSnackbar( - l10n.excessiveApiCallsWarning, - trailingIcon: Icons.settings_rounded, - trailingAction: () => navigateToSettingPage(context, LocalSettings.settingsPageFilters, settingToHighlight: LocalSettings.keywordFilters), - ); - } - return true; - }, - buildWhen: (previous, current) => - previous.status != current.status || - previous.posts != current.posts || - previous.comments != current.comments || - previous.hasReachedPostsEnd != current.hasReachedPostsEnd || - previous.hasReachedCommentsEnd != current.hasReachedCommentsEnd || - previous.feedType != current.feedType || - previous.community != current.community || - previous.communityInstance != current.communityInstance || - previous.communityModerators != current.communityModerators || - previous.user != current.user || - previous.userModerates != current.userModerates, - listener: (context, state) { - // Continue to fetch more items as long as the device view is not scrollable. - // This is to avoid cases where more items cannot be fetched because the conditions are not met - if (state.status == FeedStatus.success && ((selectedUserOption[0] && state.hasReachedPostsEnd == false) || (selectedUserOption[1] && state.hasReachedCommentsEnd == false))) { - Future.delayed(const Duration(milliseconds: 1000), () { - if (!mounted) return; - bool isScrollable = _scrollController.position.maxScrollExtent > _scrollController.position.viewportDimension; - if (!isScrollable) { - context.read().add(const FeedFetchedEvent()); - } - }); - } - - if ((state.status == FeedStatus.failure || state.status == FeedStatus.failureLoadingCommunity || state.status == FeedStatus.failureLoadingUser) && state.message != null) { - showSnackbar(state.message!); - context.read().add(FeedClearMessageEvent()); // Clear the message so that it does not spam - } - }, - builder: (context, state) { - final theme = Theme.of(context); - List posts = state.posts; - List comments = state.comments; - - return RefreshIndicator( - onRefresh: () async { - HapticFeedback.mediumImpact(); - triggerRefresh(context); - }, - edgeOffset: MediaQuery.of(context).padding.top + APP_BAR_HEIGHT, // This offset is placed to allow the correct positioning of the refresh indicator - child: Stack( - children: [ - CustomScrollView( - controller: _scrollController, - slivers: [ - widget.feedType == FeedType.account - ? AccountPageAppBar(scrollController: _scrollController) - : FeedPageAppBar(scrollController: _scrollController, scaffoldStateKey: widget.scaffoldStateKey), - // Display loading indicator until the feed is fetched - if (state.status == FeedStatus.initial) - const SliverFillRemaining( - hasScrollBody: false, - child: Center(child: CircularProgressIndicator()), + return Scaffold( + body: SafeArea( + top: false, + child: BlocConsumer( + listenWhen: (previous, current) { + if (current.excessiveApiCalls) { + showSnackbar( + l10n.excessiveApiCallsWarning, + trailingIcon: Icons.settings_rounded, + trailingAction: () => navigateToSettingPage(context, LocalSettings.settingsPageFilters, settingToHighlight: LocalSettings.keywordFilters), + ); + } + return true; + }, + buildWhen: (previous, current) => + previous.status != current.status || + previous.posts != current.posts || + previous.comments != current.comments || + previous.hasReachedPostsEnd != current.hasReachedPostsEnd || + previous.hasReachedCommentsEnd != current.hasReachedCommentsEnd || + previous.feedType != current.feedType || + previous.community != current.community || + previous.communityInstance != current.communityInstance || + previous.communityModerators != current.communityModerators || + previous.user != current.user || + previous.userModerates != current.userModerates, + listener: (context, state) { + // Continue to fetch more items as long as the device view is not scrollable. + // This is to avoid cases where more items cannot be fetched because the conditions are not met + if (state.status == FeedStatus.success && ((selectedUserOption[0] && state.hasReachedPostsEnd == false) || (selectedUserOption[1] && state.hasReachedCommentsEnd == false))) { + Future.delayed(const Duration(milliseconds: 1000), () { + if (!mounted) return; + bool isScrollable = _scrollController.position.maxScrollExtent > _scrollController.position.viewportDimension; + if (!isScrollable) { + context.read().add(const FeedFetchedEvent()); + } + }); + } + + if ((state.status == FeedStatus.failure || state.status == FeedStatus.failureLoadingCommunity || state.status == FeedStatus.failureLoadingUser) && state.message != null) { + showSnackbar(state.message!); + context.read().add(FeedClearMessageEvent()); // Clear the message so that it does not spam + } + }, + builder: (context, state) { + final theme = Theme.of(context); + List posts = state.posts; + List comments = state.comments; + + return RefreshIndicator( + onRefresh: () async { + HapticFeedback.mediumImpact(); + triggerRefresh(context); + }, + edgeOffset: MediaQuery.of(context).padding.top + APP_BAR_HEIGHT, // This offset is placed to allow the correct positioning of the refresh indicator + child: Stack( + children: [ + CustomScrollView( + controller: _scrollController, + slivers: [ + widget.feedType == FeedType.account + ? AccountPageAppBar(scrollController: _scrollController) + : FeedPageAppBar(scrollController: _scrollController, scaffoldStateKey: widget.scaffoldStateKey), + // Display loading indicator until the feed is fetched + if (state.status == FeedStatus.initial) + const SliverFillRemaining( + hasScrollBody: false, + child: Center(child: CircularProgressIndicator()), + ), + if (state.status == FeedStatus.failureLoadingCommunity || state.status == FeedStatus.failureLoadingUser) + SliverToBoxAdapter( + child: Container(), + ), + // Display tagline and list of posts once they are fetched + if (state.status != FeedStatus.initial && (state.status != FeedStatus.failureLoadingCommunity || state.status != FeedStatus.failureLoadingUser)) ...[ + SliverToBoxAdapter( + child: Visibility( + visible: state.feedType == FeedType.general && state.status != FeedStatus.initial, + child: TagLine(), ), - if (state.status == FeedStatus.failureLoadingCommunity || state.status == FeedStatus.failureLoadingUser) + ), + if (state.community != null && state.feedType == FeedType.community) SliverToBoxAdapter( - child: Container(), + child: CommunityHeader( + community: state.community!, + instance: state.communityInstance, + moderators: state.communityModerators, + condensed: false, + ), ), - // Display tagline and list of posts once they are fetched - if (state.status != FeedStatus.initial && (state.status != FeedStatus.failureLoadingCommunity || state.status != FeedStatus.failureLoadingUser)) ...[ + if (state.user != null && (state.feedType == FeedType.user || state.feedType == FeedType.account)) SliverToBoxAdapter( - child: Visibility( - visible: state.feedType == FeedType.general && state.status != FeedStatus.initial, - child: TagLine(), + child: UserHeader( + user: state.user!, + moderates: state.userModerates, + feedType: selectedUserOption[0] ? FeedTypeSubview.post : FeedTypeSubview.comment, + onChangeFeedType: (feedType) { + setState(() { + selectedUserOption[0] = feedType == FeedTypeSubview.post; + selectedUserOption[1] = feedType == FeedTypeSubview.comment; + }); + }, + condensed: false, ), ), - if (state.community != null && state.feedType == FeedType.community) - SliverToBoxAdapter( - child: CommunityHeader( - community: state.community!, - instance: state.communityInstance, - moderators: state.communityModerators, - condensed: false, + selectedUserOption[1] + // Widget representing the list of user comments on the feed + ? FeedCommentCardList( + comments: comments, + tabletMode: tabletMode, + ) + : + // Widget representing the list of posts on the feed + FeedPostCardList( + posts: posts, + tabletMode: tabletMode, + markPostReadOnScroll: markPostReadOnScroll, + queuedForRemoval: queuedForRemoval, + dimReadPosts: state.feedType == FeedType.account ? false : null, ), - ), - if (state.user != null && (state.feedType == FeedType.user || state.feedType == FeedType.account)) - SliverToBoxAdapter( - child: UserHeader( - user: state.user!, - moderates: state.userModerates, - feedType: selectedUserOption[0] ? FeedTypeSubview.post : FeedTypeSubview.comment, - onChangeFeedType: (feedType) { - setState(() { - selectedUserOption[0] = feedType == FeedTypeSubview.post; - selectedUserOption[1] = feedType == FeedTypeSubview.comment; - }); - }, - condensed: false, - ), - ), - selectedUserOption[1] - // Widget representing the list of user comments on the feed - ? FeedCommentCardList( - comments: comments, - tabletMode: tabletMode, - ) - : - // Widget representing the list of posts on the feed - FeedPostCardList( - posts: posts, - tabletMode: tabletMode, - markPostReadOnScroll: markPostReadOnScroll, - queuedForRemoval: queuedForRemoval, - dimReadPosts: state.feedType == FeedType.account ? false : null, - ), - // Widget representing the bottom of the feed (reached end or loading more posts indicators) - if (state.status != FeedStatus.failureLoadingCommunity && state.status != FeedStatus.failureLoadingUser) - SliverToBoxAdapter( - child: ((selectedUserOption[0] && state.hasReachedPostsEnd) || (selectedUserOption[1] && state.hasReachedCommentsEnd)) - ? const FeedReachedEnd() - : Container( - height: state.status == FeedStatus.initial ? MediaQuery.of(context).size.height * 0.5 : null, // Might have to adjust this to be more robust - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: const CircularProgressIndicator(), - ), - ), - ], + // Widget representing the bottom of the feed (reached end or loading more posts indicators) + if (state.status != FeedStatus.failureLoadingCommunity && state.status != FeedStatus.failureLoadingUser) + SliverToBoxAdapter( + child: ((selectedUserOption[0] && state.hasReachedPostsEnd) || (selectedUserOption[1] && state.hasReachedCommentsEnd)) + ? const FeedReachedEnd() + : Container( + height: state.status == FeedStatus.initial ? MediaQuery.of(context).size.height * 0.5 : null, // Might have to adjust this to be more robust + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: const CircularProgressIndicator(), + ), + ), + ], + ], + ), + // Widget to host the feed FAB when navigating to new page + AnimatedOpacity( + opacity: isFabOpen ? 1.0 : 0.0, + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 250), + child: Stack( + children: [ + IgnorePointer( + child: Container( + color: theme.colorScheme.surface.withValues(alpha: 0.95), + )), + if (isFabOpen) + ModalBarrier( + color: null, + dismissible: true, + onDismiss: () => context.read().setFeedFabOpen(false), + ), ], ), - // Widget to host the feed FAB when navigating to new page + ), + if (Navigator.of(context).canPop() && + (state.communityId != null || state.communityName != null || state.userId != null || state.username != null) && + enableFeedsFab && + state.feedType != FeedType.account) AnimatedOpacity( - opacity: isFabOpen ? 1.0 : 0.0, - curve: Curves.easeInOut, - duration: const Duration(milliseconds: 250), - child: Stack( - children: [ - IgnorePointer( - child: Container( - color: theme.colorScheme.surface.withValues(alpha: 0.95), - )), - if (isFabOpen) - ModalBarrier( - color: null, - dismissible: true, - onDismiss: () => context.read().setFeedFabOpen(false), - ), - ], + opacity: enableFeedsFab ? 1.0 : 0.0, + duration: const Duration(milliseconds: 150), + curve: Curves.easeIn, + child: Container( + margin: const EdgeInsets.all(16), + child: FeedFAB(heroTag: state.communityName ?? state.username), ), ), - if (Navigator.of(context).canPop() && - (state.communityId != null || state.communityName != null || state.userId != null || state.username != null) && - enableFeedsFab && - state.feedType != FeedType.account) - AnimatedOpacity( - opacity: enableFeedsFab ? 1.0 : 0.0, - duration: const Duration(milliseconds: 150), - curve: Curves.easeIn, - child: Container( - margin: const EdgeInsets.all(16), - child: FeedFAB(heroTag: state.communityName ?? state.username), - ), + if (hideTopBarOnScroll) + Positioned( + child: Container( + height: MediaQuery.of(context).padding.top, + color: theme.colorScheme.surface, ), - if (hideTopBarOnScroll) - Positioned( - child: Container( - height: MediaQuery.of(context).padding.top, - color: theme.colorScheme.surface, - ), - ) - ], - ), - ); - }, - ), + ) + ], + ), + ); + }, ), ), ); diff --git a/lib/src/features/feed/presentation/state/feed_bloc.dart b/lib/src/features/feed/presentation/state/feed_bloc.dart index a7e0eae06..b33102224 100644 --- a/lib/src/features/feed/presentation/state/feed_bloc.dart +++ b/lib/src/features/feed/presentation/state/feed_bloc.dart @@ -615,6 +615,7 @@ class FeedBloc extends Bloc { } FeedResult feedItemResult = await fetchFeedItems( + account: account, cursor: null, feedListType: event.feedListType, postSortType: event.postSortType, @@ -671,6 +672,7 @@ class FeedBloc extends Bloc { List comments = List.from(state.comments); FeedResult feedItemResult = await fetchFeedItems( + account: account, cursor: state.cursor, feedListType: state.feedListType, postSortType: state.postSortType, diff --git a/lib/src/features/feed/presentation/utils/community_feed_utils.dart b/lib/src/features/feed/presentation/utils/community_feed_utils.dart index 937b530bd..0353d5e47 100644 --- a/lib/src/features/feed/presentation/utils/community_feed_utils.dart +++ b/lib/src/features/feed/presentation/utils/community_feed_utils.dart @@ -6,6 +6,7 @@ import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/account/account.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/foundation/networking/networking.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; @@ -14,7 +15,7 @@ import 'package:thunder/packages/ui/ui.dart' show showSnackbar, showThunderDialo Future toggleFavoriteCommunity(BuildContext context, ThunderCommunity community, bool isFavorite) async { try { final l10n = AppLocalizations.of(GlobalContext.context)!; - final account = await fetchActiveProfile(); + final account = resolveEffectiveAccount(context); if (account.anonymous) throw Exception(l10n.userNotLoggedIn); if (isFavorite) { @@ -88,7 +89,7 @@ Future handleSubscription(BuildContext context, ThunderCommun if (!shouldUnsubscribe) return null; } - final account = context.read().state.account; + final account = resolveEffectiveAccount(context); final repository = CommunityRepositoryImpl(account: account); return await repository.subscribe(community.id, !isSubscribed); } diff --git a/lib/src/features/feed/presentation/utils/feed_fetch_utils.dart b/lib/src/features/feed/presentation/utils/feed_fetch_utils.dart index 4726acbab..90790303c 100644 --- a/lib/src/features/feed/presentation/utils/feed_fetch_utils.dart +++ b/lib/src/features/feed/presentation/utils/feed_fetch_utils.dart @@ -10,6 +10,7 @@ import 'package:thunder/src/features/user/user.dart'; /// Helper function which handles the logic of fetching items for the feed from the API /// This includes posts and user information (posts/comments) Future fetchFeedItems({ + required Account account, String? cursor, FeedListType? feedListType, PostSortType? postSortType, @@ -22,8 +23,6 @@ Future fetchFeedItems({ bool showSaved = false, void Function()? notifyExcessiveApiCalls, }) async { - final account = await fetchActiveProfile(); - List keywordFilters = UserPreferences.getLocalSetting(LocalSettings.keywordFilters) ?? []; int desiredPosts = 20; diff --git a/lib/src/features/feed/presentation/utils/feed_share_utils.dart b/lib/src/features/feed/presentation/utils/feed_share_utils.dart index 3e248b574..ed9accc42 100644 --- a/lib/src/features/feed/presentation/utils/feed_share_utils.dart +++ b/lib/src/features/feed/presentation/utils/feed_share_utils.dart @@ -3,17 +3,17 @@ import 'package:flutter/material.dart'; import 'package:share_plus/share_plus.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/src/features/feed/presentation/models/feed_share_options.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/packages/ui/ui.dart' show BottomSheetListPicker, ListPickerItem; /// Shows a bottom modal sheet which allows sharing the given [community]. Future showCommunityShareSheet(BuildContext context, ThunderCommunity community) async { final l10n = AppLocalizations.of(context)!; - final account = await fetchActiveProfile(); + final account = resolveEffectiveAccount(context); final communityLink = await getLemmyCommunity(community.actorId) ?? ''; final lemmyLink = '!$communityLink'; @@ -74,7 +74,7 @@ Future showCommunityShareSheet(BuildContext context, ThunderCommunity comm /// Shows a bottom modal sheet which allows sharing the given [person]. Future showUserShareSheet(BuildContext context, ThunderUser person) async { final AppLocalizations l10n = AppLocalizations.of(context)!; - final account = await fetchActiveProfile(); + final account = resolveEffectiveAccount(context); String user = await getLemmyUser(person.actorId) ?? ''; String lemmyLink = '@$user'; diff --git a/lib/src/features/feed/presentation/widgets/feed_fab.dart b/lib/src/features/feed/presentation/widgets/feed_fab.dart index 3bd56ff7a..aec195727 100644 --- a/lib/src/features/feed/presentation/widgets/feed_fab.dart +++ b/lib/src/features/feed/presentation/widgets/feed_fab.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; +import 'package:thunder/src/app/shell/state/shell_chrome_cubit.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; @@ -18,16 +19,21 @@ import 'package:thunder/src/shared/sort_picker.dart'; import 'package:thunder/packages/ui/ui.dart' show showSnackbar; class FeedFAB extends StatelessWidget { - const FeedFAB({super.key, this.heroTag}); + const FeedFAB({super.key, this.heroTag, this.actionController}); final String? heroTag; + final FeedActionController? actionController; + + FeedActionController? _resolveActionController(BuildContext context) { + return actionController ?? FeedActionScope.maybeOf(context); + } @override build(BuildContext context) { final theme = Theme.of(context); final feedFabSinglePressAction = context.select((cubit) => cubit.state.feedFabSinglePressAction); final feedFabLongPressAction = context.select((cubit) => cubit.state.feedFabLongPressAction); - final isFabSummoned = context.select((cubit) => cubit.state.isFeedFabSummoned); + final isFabSummoned = context.select((cubit) => cubit.state.isFeedFabSummoned); final FeedState feedState = context.watch().state; final ProfileState profileState = context.read().state; @@ -180,7 +186,7 @@ class FeedFAB extends StatelessWidget { child: GestureDetector( onVerticalDragEnd: (DragEndDetails details) { if (details.primaryVelocity! < 0) { - context.read().setFeedFabSummoned(true); + context.read().setFeedFabSummoned(true); } }, ), @@ -261,11 +267,11 @@ class FeedFAB extends StatelessWidget { } Future triggerOpenFab(BuildContext context) async { - context.read().setFeedFabOpen(true); + context.read().setFeedFabOpen(true); } Future triggerDismissRead(BuildContext context) async { - context.read().dismissRead(); + await _resolveActionController(context)?.dismissRead(); } Future triggerChangeSort(BuildContext context) async { @@ -290,7 +296,7 @@ class FeedFAB extends StatelessWidget { } Future triggerScrollToTop(BuildContext context) async { - context.read().scrollToTop(); + await _resolveActionController(context)?.scrollToTop(); } Future triggerNewPost(BuildContext context, {bool isPostingLocked = false}) async { diff --git a/lib/src/features/feed/presentation/widgets/feed_page_app_bar.dart b/lib/src/features/feed/presentation/widgets/feed_page_app_bar.dart index 679e2377d..897541d6a 100644 --- a/lib/src/features/feed/presentation/widgets/feed_page_app_bar.dart +++ b/lib/src/features/feed/presentation/widgets/feed_page_app_bar.dart @@ -60,7 +60,7 @@ class _FeedPageAppBarState extends State { @override Widget build(BuildContext context) { final feedBloc = context.read(); - final thunderBloc = context.read(); + final thunderBloc = context.read(); final ProfileState profileState = context.read().state; return BlocListener( diff --git a/lib/src/features/feed/presentation/widgets/feed_post_card_list.dart b/lib/src/features/feed/presentation/widgets/feed_post_card_list.dart index 1b5a21676..14f90f26f 100644 --- a/lib/src/features/feed/presentation/widgets/feed_post_card_list.dart +++ b/lib/src/features/feed/presentation/widgets/feed_post_card_list.dart @@ -28,7 +28,7 @@ class FeedPostCardList extends StatefulWidget { /// The list of posts to show on the feed final List posts; - /// Whether or not to dim read posts. This value overrides [dimReadPosts] in [ThunderBloc] + /// Whether or not to dim read posts. This value overrides [dimReadPosts] in [ThunderCubit] final bool? dimReadPosts; /// Whether to disable swiping of posts @@ -37,6 +37,36 @@ class FeedPostCardList extends StatefulWidget { /// Overrides the system setting for whether to indicate read posts final bool? indicateRead; + /// Optional feed type override for contexts without a FeedBloc. + final FeedType? feedType; + + /// Optional feed list type override for contexts without a FeedBloc. + final FeedListType? feedListType; + + /// Optional callback for voting a post. + final Future Function(ThunderPost post, int voteType)? onVoteAction; + + /// Optional callback for saving a post. + final Future Function(ThunderPost post, bool saved)? onSaveAction; + + /// Optional callback for toggling post read state. + final Future Function(ThunderPost post, bool read)? onReadAction; + + /// Optional callback for toggling post hidden state. + final Future Function(ThunderPost post, bool hidden)? onHideAction; + + /// Optional callback for marking multiple posts read. + final Future Function(List postIds, bool read)? onMultiReadAction; + + /// Optional callback for replacing a post in the current list. + final void Function(ThunderPost post)? onPostUpdated; + + /// Optional callback for dismissing a hidden post from view. + final void Function(int postId)? onDismissHiddenPost; + + /// Optional callback for dismissing blocked content from view. + final void Function({int? userId, int? communityId})? onDismissBlocked; + const FeedPostCardList({ super.key, required this.posts, @@ -46,6 +76,16 @@ class FeedPostCardList extends StatefulWidget { this.dimReadPosts, this.disableSwiping = false, this.indicateRead, + this.feedType, + this.feedListType, + this.onVoteAction, + this.onSaveAction, + this.onReadAction, + this.onHideAction, + this.onMultiReadAction, + this.onPostUpdated, + this.onDismissHiddenPost, + this.onDismissBlocked, }); @override @@ -93,16 +133,39 @@ class _FeedPostCardListState extends State { }) { Widget child = PostCard( post: post, - onVoteAction: (int voteType) { + feedType: feedType, + feedListType: feedListType, + onVoteAction: (int voteType) async { + if (widget.onVoteAction != null) { + await widget.onVoteAction!(post, voteType); + return; + } + context.read().add(FeedItemActionedEvent(postId: post.id, postAction: PostAction.vote, actionInput: VotePostInput(voteType))); }, - onSaveAction: (bool saved) { + onSaveAction: (bool saved) async { + if (widget.onSaveAction != null) { + await widget.onSaveAction!(post, saved); + return; + } + context.read().add(FeedItemActionedEvent(postId: post.id, postAction: PostAction.save, actionInput: SavePostInput(saved))); }, - onReadAction: (bool read) { + onReadAction: (bool read) async { + if (widget.onReadAction != null) { + await widget.onReadAction!(post, read); + return; + } + context.read().add(FeedItemActionedEvent(postId: post.id, postAction: PostAction.read, actionInput: ReadPostInput(read))); }, - onHideAction: (bool hide) { + onHideAction: (bool hide) async { + if (widget.onHideAction != null) { + await widget.onHideAction!(post, hide); + widget.onDismissHiddenPost?.call(post.id); + return; + } + context.read().add(FeedItemActionedEvent(postId: post.id, postAction: PostAction.hide, actionInput: HidePostInput(hide))); context.read().add(FeedDismissHiddenPostEvent(postId: post.id)); }, @@ -122,6 +185,9 @@ class _FeedPostCardListState extends State { indicateRead: dim, isLastTapped: lastTappedPost == post.id, disableSwiping: widget.disableSwiping, + onPostUpdated: widget.onPostUpdated, + onDismissHiddenPost: widget.onDismissHiddenPost, + onDismissBlocked: widget.onDismissBlocked, ); // Apply VisibilityDetector if [markPostReadOnScroll] is enabled @@ -155,7 +221,11 @@ class _FeedPostCardListState extends State { if (index > lastProcessedIndex) lastProcessedIndex = index; if (markReadPostIds.isNotEmpty) { - context.read().add(FeedItemActionedEvent(postIds: [...markReadPostIds], postAction: PostAction.multiRead, actionInput: const MultiReadPostInput(true))); + if (widget.onMultiReadAction != null) { + widget.onMultiReadAction!([...markReadPostIds], true); + } else { + context.read().add(FeedItemActionedEvent(postIds: [...markReadPostIds], postAction: PostAction.multiRead, actionInput: const MultiReadPostInput(true))); + } readPostIds.addAll(markReadPostIds); // Add all post ids that were queued to prevent them from being queued again markReadPostIds = {}; // Reset the list of post ids to mark as read } @@ -196,8 +266,9 @@ class _FeedPostCardListState extends State { @override Widget build(BuildContext context) { - final feedType = context.select((bloc) => bloc.state.feedType); - final feedListType = context.select((bloc) => bloc.state.feedListType); + final hasFeedBloc = context.findAncestorWidgetOfExactType>() != null; + final feedType = widget.feedType ?? (hasFeedBloc ? context.select((bloc) => bloc.state.feedType) : null); + final feedListType = widget.feedListType ?? (hasFeedBloc ? context.select((bloc) => bloc.state.feedListType) : null); final isUserLoggedIn = context.select((bloc) => bloc.state.isLoggedIn); bool dimReadPosts = widget.dimReadPosts ?? (isUserLoggedIn && context.select((cubit) => cubit.state.dimReadPosts)); diff --git a/lib/src/features/instance/domain/utils/instance_link_utils.dart b/lib/src/features/instance/domain/utils/instance_link_utils.dart index f58863552..426ed8e9c 100644 --- a/lib/src/features/instance/domain/utils/instance_link_utils.dart +++ b/lib/src/features/instance/domain/utils/instance_link_utils.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/search/search.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/app/shell/navigation/loading_page.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; import 'package:thunder/src/features/instance/data/services/instance_discovery_service.dart' as instance_discovery; @@ -42,7 +40,7 @@ Future getLemmyPostId(BuildContext context, String text) async { return null; } - final account = context.read().state.account; + final account = resolveEffectiveAccount(context); final postId = int.tryParse(parsed.value); if (postId == null) { @@ -71,7 +69,7 @@ Future getLemmyCommentId(BuildContext context, String text) async { return null; } - final account = context.read().state.account; + final account = resolveEffectiveAccount(context); final commentId = int.tryParse(parsed.value); if (commentId == null) { diff --git a/lib/src/features/instance/presentation/pages/instance_page.dart b/lib/src/features/instance/presentation/pages/instance_page.dart index 0f069f0ec..dc2892fd7 100644 --- a/lib/src/features/instance/presentation/pages/instance_page.dart +++ b/lib/src/features/instance/presentation/pages/instance_page.dart @@ -6,7 +6,6 @@ import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/app/wiring/state_factories.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/account/account.dart'; -import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/features/instance/instance.dart'; import 'package:thunder/src/features/instance/presentation/state/instance_page_bloc.dart'; import 'package:thunder/src/features/instance/presentation/state/instance_page_event.dart'; @@ -17,11 +16,14 @@ import 'package:thunder/src/features/instance/presentation/widgets/instance_tabs /// /// The page contains information about a given instance, with the ability to explore its content. class InstancePage extends StatefulWidget { + final Account account; + /// The instance to display. final ThunderInstanceInfo instance; const InstancePage({ super.key, + required this.account, required this.instance, }); @@ -114,7 +116,7 @@ class _InstancePageState extends State with SingleTickerProviderSt Widget build(BuildContext context) { final l10n = GlobalContext.l10n; - final account = context.read().state.account; + final account = widget.account; return MultiBlocProvider( providers: [ @@ -124,12 +126,8 @@ class _InstancePageState extends State with SingleTickerProviderSt instanceInfo: widget.instance, ), ), - BlocProvider(create: (context) => createFeedBloc(account)), ], - child: BlocConsumer( - listener: (context, state) { - context.read().add(PopulatePostsEvent(state.posts.items)); - }, + child: BlocBuilder( builder: (context, state) { buildContext = context; diff --git a/lib/src/features/instance/presentation/widgets/instance_action_bottom_sheet.dart b/lib/src/features/instance/presentation/widgets/instance_action_bottom_sheet.dart index af8b49475..4b226aed5 100644 --- a/lib/src/features/instance/presentation/widgets/instance_action_bottom_sheet.dart +++ b/lib/src/features/instance/presentation/widgets/instance_action_bottom_sheet.dart @@ -67,6 +67,7 @@ enum InstanceBottomSheetAction { class InstanceActionBottomSheet extends StatefulWidget { const InstanceActionBottomSheet({ super.key, + required this.context, required this.account, required this.blockedInstances, this.communityInstanceId, @@ -76,6 +77,9 @@ class InstanceActionBottomSheet extends StatefulWidget { this.onAction, }); + /// The account to use for the instance actions + final BuildContext context; + /// The account to use for the instance actions final Account account; @@ -111,7 +115,7 @@ class _InstanceActionBottomSheetState extends State { switch (action) { case InstanceBottomSheetAction.visitCommunityInstance: - navigateToInstancePage(context, instanceHost: fetchInstanceNameFromUrl(widget.communityInstanceUrl)!, instanceId: widget.communityInstanceId); + navigateToInstancePage(widget.context, account: widget.account, instanceHost: fetchInstanceNameFromUrl(widget.communityInstanceUrl)!, instanceId: widget.communityInstanceId); break; case InstanceBottomSheetAction.blockCommunityInstance: Navigator.of(context).pop(); @@ -130,7 +134,7 @@ class _InstanceActionBottomSheetState extends State { widget.onAction?.call(); break; case InstanceBottomSheetAction.visitUserInstance: - navigateToInstancePage(context, instanceHost: fetchInstanceNameFromUrl(widget.userInstanceUrl)!, instanceId: widget.userInstanceId); + navigateToInstancePage(widget.context, account: widget.account, instanceHost: fetchInstanceNameFromUrl(widget.userInstanceUrl)!, instanceId: widget.userInstanceId); break; case InstanceBottomSheetAction.blockUserInstance: Navigator.of(context).pop(); diff --git a/lib/src/features/instance/presentation/widgets/instance_tabs.dart b/lib/src/features/instance/presentation/widgets/instance_tabs.dart index d631ee534..6d2b0474c 100644 --- a/lib/src/features/instance/presentation/widgets/instance_tabs.dart +++ b/lib/src/features/instance/presentation/widgets/instance_tabs.dart @@ -9,6 +9,7 @@ import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/src/features/feed/feed.dart'; +import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/features/instance/presentation/state/instance_page_bloc.dart'; import 'package:thunder/src/features/instance/presentation/state/instance_page_event.dart'; @@ -180,7 +181,7 @@ class InstanceUserTab extends StatelessWidget { } } -class InstancePostTab extends StatelessWidget { +class InstancePostTab extends StatefulWidget { /// The account to use for the tab. final Account account; @@ -195,24 +196,68 @@ class InstancePostTab extends StatelessWidget { const InstancePostTab({super.key, required this.account, required this.searchSortType, required this.onRetry, this.query}); + @override + State createState() => _InstancePostTabState(); +} + +class _InstancePostTabState extends State { + late final PostListActionController _postListActionController; + List _posts = const []; + + @override + void initState() { + super.initState(); + _postListActionController = PostListActionController(postRepository: PostRepositoryImpl(account: widget.account)); + _posts = context.read().state.posts.items; + } + + void _setPosts(List posts) { + if (!mounted) return; + setState(() => _posts = posts); + } + + void _syncSourcePosts(List sourcePosts) { + _posts = _postListActionController.reconcile(sourcePosts: sourcePosts, currentPosts: _posts); + } + + Future _handleVoteAction(ThunderPost post, int voteType) async => _setPosts(await _postListActionController.vote(_posts, post, voteType)); + + Future _handleSaveAction(ThunderPost post, bool saved) async => _setPosts(await _postListActionController.save(_posts, post, saved)); + + Future _handleReadAction(ThunderPost post, bool read) async => _setPosts(await _postListActionController.read(_posts, post, read)); + + Future _handleHideAction(ThunderPost post, bool hidden) async => _setPosts(await _postListActionController.hide(_posts, post, hidden)); + + Future _handleMultiReadAction(List postIds, bool read) async => _setPosts(await _postListActionController.multiRead(_posts, postIds, read)); + @override Widget build(BuildContext context) { - final tabletMode = context.read().state.tabletMode; + final tabletMode = context.read().state.tabletMode; return BlocBuilder( buildWhen: (previous, current) => previous.posts != current.posts, builder: (context, state) { + _syncSourcePosts(state.posts.items); + return _InstanceTabScaffold( state: state.posts, storageKey: 'posts', - onRetry: onRetry, - onLoadMore: () => context.read().add(GetInstancePosts(page: state.posts.page + 1, sortType: searchSortType, query: query)), + onRetry: widget.onRetry, + onLoadMore: () => context.read().add(GetInstancePosts(page: state.posts.page + 1, sortType: widget.searchSortType, query: widget.query)), loadingWidget: SliverMainAxisGroup( slivers: [ FeedPostCardList( markPostReadOnScroll: false, - posts: state.posts.items, + posts: _posts, tabletMode: tabletMode, + onVoteAction: _handleVoteAction, + onSaveAction: _handleSaveAction, + onReadAction: _handleReadAction, + onHideAction: _handleHideAction, + onMultiReadAction: _handleMultiReadAction, + onPostUpdated: (post) => _setPosts(_postListActionController.updatePost(_posts, post)), + onDismissHiddenPost: (postId) => _setPosts(_postListActionController.dismissHiddenPost(_posts, postId)), + onDismissBlocked: ({userId, communityId}) => _setPosts(_postListActionController.dismissBlocked(_posts, userId: userId, communityId: communityId)), ), if (state.posts.status == InstancePageStatus.loading) const SliverToBoxAdapter(child: Center(child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator()))), ], diff --git a/lib/src/features/moderator/presentation/pages/report_page.dart b/lib/src/features/moderator/presentation/pages/report_page.dart index 606ee46c0..822371cb9 100644 --- a/lib/src/features/moderator/presentation/pages/report_page.dart +++ b/lib/src/features/moderator/presentation/pages/report_page.dart @@ -14,6 +14,7 @@ import 'package:thunder/src/features/moderator/moderator.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; import 'package:thunder/src/features/comment/presentation/widgets/comment_reference.dart'; import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/features/settings/api.dart'; @@ -35,8 +36,10 @@ class ReportFeedPage extends StatefulWidget { class _ReportFeedPageState extends State { @override Widget build(BuildContext context) { + final account = resolveActiveAccount(context); + return BlocProvider( - create: (_) => createReportBloc()..add(const ReportFeedFetchedEvent(reportFeedType: ReportFeedType.post, reset: true)), + create: (_) => createReportBloc(account)..add(const ReportFeedFetchedEvent(reportFeedType: ReportFeedType.post, reset: true)), child: const ReportFeedView(), ); } @@ -131,6 +134,7 @@ class _ReportFeedViewState extends State { showDragHandle: true, context: context, builder: (builderContext) => ReportFilterBottomSheet( + account: context.read().account, status: showResolved ? ReportResolveStatus.all : ReportResolveStatus.unresolved, onSubmit: (ReportResolveStatus status, ThunderCommunity? community) async => { HapticFeedback.mediumImpact(), diff --git a/lib/src/features/moderator/presentation/state/report_bloc.dart b/lib/src/features/moderator/presentation/state/report_bloc.dart index cb7de67c9..d4527356f 100644 --- a/lib/src/features/moderator/presentation/state/report_bloc.dart +++ b/lib/src/features/moderator/presentation/state/report_bloc.dart @@ -19,7 +19,7 @@ EventTransformer throttleDroppable(Duration duration) { } class ReportBloc extends Bloc { - ReportBloc({required LocalizationService localizationService}) + ReportBloc({required this.account, required LocalizationService localizationService}) : _localizationService = localizationService, super(const ReportState()) { /// Handles resetting the report feed to its initial state @@ -53,6 +53,7 @@ class ReportBloc extends Bloc { ); } + final Account account; final LocalizationService _localizationService; /// Handles clearing any messages from the state @@ -103,6 +104,7 @@ class ReportBloc extends Bloc { if (state.status != ReportStatus.initial) add(ResetReportEvent()); Map fetchReportsResult = await fetchReports( + account: account, page: 1, unresolved: !event.showResolved, communityId: event.communityId, @@ -150,6 +152,7 @@ class ReportBloc extends Bloc { List commentReportViews = List.from(state.commentReports); Map fetchReportsResult = await fetchReports( + account: account, page: state.currentPage, unresolved: !state.showResolved, communityId: state.communityId, @@ -242,7 +245,7 @@ class ReportBloc extends Bloc { emit(state.copyWith(status: ReportStatus.success, postReports: optimisticPostReports)); emit(state.copyWith(status: ReportStatus.fetching, postReports: optimisticPostReports)); - bool success = await resolvePostReport(postReportView.id, value); + bool success = await resolvePostReport(account, postReportView.id, value); if (success) { return emit(state.copyWith( status: ReportStatus.success, @@ -308,7 +311,7 @@ class ReportBloc extends Bloc { emit(state.copyWith(status: ReportStatus.success, commentReports: optimisticCommentReports)); emit(state.copyWith(status: ReportStatus.fetching, commentReports: optimisticCommentReports)); - bool success = await resolveCommentReport(originalCommentReport.id, value); + bool success = await resolveCommentReport(account, originalCommentReport.id, value); if (success) { return emit(state.copyWith( status: ReportStatus.success, diff --git a/lib/src/features/moderator/presentation/utils/report_actions_utils.dart b/lib/src/features/moderator/presentation/utils/report_actions_utils.dart index 2e4f11e31..3660a59ea 100644 --- a/lib/src/features/moderator/presentation/utils/report_actions_utils.dart +++ b/lib/src/features/moderator/presentation/utils/report_actions_utils.dart @@ -5,6 +5,7 @@ import 'package:thunder/src/features/post/post.dart'; /// Helper function which handles the logic of fetching post/comment reports Future> fetchReports({ + required Account account, int page = 1, int limit = 10, bool unresolved = false, @@ -13,8 +14,6 @@ Future> fetchReports({ int? commentId, ReportFeedType reportFeedType = ReportFeedType.post, }) async { - final account = await fetchActiveProfile(); - bool hasReachedPostReportsEnd = false; bool hasReachedCommentReportsEnd = false; @@ -64,8 +63,7 @@ ThunderPostReport optimisticallyResolvePostReport(ThunderPostReport postReport, } /// Logic to resolve a post report -Future resolvePostReport(int postReportId, bool resolved) async { - final account = await fetchActiveProfile(); +Future resolvePostReport(Account account, int postReportId, bool resolved) async { final postReportResponse = await PostRepositoryImpl(account: account).resolvePostReport(postReportId, resolved); return postReportResponse.resolved == resolved; @@ -77,8 +75,7 @@ ThunderCommentReport optimisticallyResolveCommentReport(ThunderCommentReport com } /// Logic to resolve a comment report -Future resolveCommentReport(int commentReportId, bool resolved) async { - final account = await fetchActiveProfile(); +Future resolveCommentReport(Account account, int commentReportId, bool resolved) async { final commentReportResponse = await CommentRepositoryImpl(account: account).resolveCommentReport(commentReportId, resolved); return commentReportResponse.resolved == resolved; diff --git a/lib/src/features/moderator/presentation/widgets/report_page_filter_bottom_sheet.dart b/lib/src/features/moderator/presentation/widgets/report_page_filter_bottom_sheet.dart index c5a601f00..7a5b73cd4 100644 --- a/lib/src/features/moderator/presentation/widgets/report_page_filter_bottom_sheet.dart +++ b/lib/src/features/moderator/presentation/widgets/report_page_filter_bottom_sheet.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; @@ -13,7 +11,9 @@ enum ReportResolveStatus { unresolved, all } /// A [BottomSheet] that allows the user to filter reports by status and community /// When the submit button is pressed, the [onSubmit] function is called with the selected [ReportResolveStatus] and [ThunderCommunity] if any. class ReportFilterBottomSheet extends StatefulWidget { - const ReportFilterBottomSheet({super.key, required this.status, required this.onSubmit}); + const ReportFilterBottomSheet({super.key, required this.account, required this.status, required this.onSubmit}); + + final Account account; /// The status to filter by final ReportResolveStatus status; @@ -80,7 +80,7 @@ class _ReportFilterBottomSheetState extends State { Text(l10n.community, style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8.0), CommunitySelector( - account: context.read().state.account, + account: widget.account, community: community, onCommunitySelected: (ThunderCommunity c) { setState(() => community = c); diff --git a/lib/src/features/modlog/presentation/pages/modlog_page.dart b/lib/src/features/modlog/presentation/pages/modlog_page.dart index 674e5e1b4..cb9065ab7 100644 --- a/lib/src/features/modlog/presentation/pages/modlog_page.dart +++ b/lib/src/features/modlog/presentation/pages/modlog_page.dart @@ -15,6 +15,7 @@ import 'package:thunder/packages/ui/ui.dart' show showSnackbar; class ModlogFeedPage extends StatefulWidget { const ModlogFeedPage({ super.key, + required this.account, this.modlogActionType, this.communityId, this.userId, @@ -23,6 +24,9 @@ class ModlogFeedPage extends StatefulWidget { required this.subtitle, }); + /// The filtering to be applied to the feed. + final Account account; + /// The filtering to be applied to the feed. final ModlogActionType? modlogActionType; @@ -49,30 +53,18 @@ class ModlogFeedPage extends StatefulWidget { class _ModlogFeedPageState extends State { @override Widget build(BuildContext context) { - return FutureBuilder( - future: fetchActiveProfile(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Scaffold( - body: Center(child: CircularProgressIndicator()), - ); - } - - final account = snapshot.data!; - return BlocProvider( - create: (_) => ModlogCubit( - repository: ModlogRepositoryImpl(account: account), - )..fetchModlogFeed( - modlogActionType: widget.modlogActionType, - communityId: widget.communityId, - userId: widget.userId, - moderatorId: widget.moderatorId, - commentId: widget.commentId, - reset: true, - ), - child: ModlogFeedView(subtitle: widget.subtitle), - ); - }, + return BlocProvider( + create: (_) => ModlogCubit( + repository: ModlogRepositoryImpl(account: widget.account), + )..fetchModlogFeed( + modlogActionType: widget.modlogActionType, + communityId: widget.communityId, + userId: widget.userId, + moderatorId: widget.moderatorId, + commentId: widget.commentId, + reset: true, + ), + child: ModlogFeedView(subtitle: widget.subtitle), ); } } @@ -120,7 +112,7 @@ class _ModlogFeedViewState extends State { @override Widget build(BuildContext context) { - final hideTopBarOnScroll = context.select((bloc) => bloc.state.hideTopBarOnScroll); + final hideTopBarOnScroll = context.select((bloc) => bloc.state.hideTopBarOnScroll); return Scaffold( body: SafeArea( diff --git a/lib/src/features/modlog/presentation/state/modlog_cubit.freezed.dart b/lib/src/features/modlog/presentation/state/modlog_cubit.freezed.dart index fe59261da..d5360b136 100644 --- a/lib/src/features/modlog/presentation/state/modlog_cubit.freezed.dart +++ b/lib/src/features/modlog/presentation/state/modlog_cubit.freezed.dart @@ -1,5 +1,5 @@ -// coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark @@ -9,52 +9,77 @@ part of 'modlog_cubit.dart'; // FreezedGenerator // ************************************************************************** +// dart format off T _$identity(T value) => value; -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - /// @nodoc mixin _$ModlogState { /// The status of the modlog feed. - ModlogStatus get status => throw _privateConstructorUsedError; + ModlogStatus get status; /// The type of modlog action to filter by. - ModlogActionType get modlogActionType => throw _privateConstructorUsedError; + ModlogActionType get modlogActionType; /// A community ID to filter the modlog by. - int? get communityId => throw _privateConstructorUsedError; + int? get communityId; /// A user ID to filter the modlog by. - int? get userId => throw _privateConstructorUsedError; + int? get userId; /// A moderator ID to filter the modlog by. - int? get moderatorId => throw _privateConstructorUsedError; + int? get moderatorId; /// A comment ID to filter the modlog by. - int? get commentId => throw _privateConstructorUsedError; + int? get commentId; /// The list of modlog events. - List get modlogEventItems => throw _privateConstructorUsedError; + List get modlogEventItems; /// Whether the end of the modlog has been reached. - bool get hasReachedEnd => throw _privateConstructorUsedError; + bool get hasReachedEnd; /// The current page of the modlog. - int get currentPage => throw _privateConstructorUsedError; + int get currentPage; /// The error message to display after a failure. - String? get message => throw _privateConstructorUsedError; + String? get message; /// Create a copy of ModlogState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) - $ModlogStateCopyWith get copyWith => throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + $ModlogStateCopyWith get copyWith => _$ModlogStateCopyWithImpl(this as ModlogState, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is ModlogState && + (identical(other.status, status) || other.status == status) && + (identical(other.modlogActionType, modlogActionType) || other.modlogActionType == modlogActionType) && + (identical(other.communityId, communityId) || other.communityId == communityId) && + (identical(other.userId, userId) || other.userId == userId) && + (identical(other.moderatorId, moderatorId) || other.moderatorId == moderatorId) && + (identical(other.commentId, commentId) || other.commentId == commentId) && + const DeepCollectionEquality().equals(other.modlogEventItems, modlogEventItems) && + (identical(other.hasReachedEnd, hasReachedEnd) || other.hasReachedEnd == hasReachedEnd) && + (identical(other.currentPage, currentPage) || other.currentPage == currentPage) && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => + Object.hash(runtimeType, status, modlogActionType, communityId, userId, moderatorId, commentId, const DeepCollectionEquality().hash(modlogEventItems), hasReachedEnd, currentPage, message); + + @override + String toString() { + return 'ModlogState(status: $status, modlogActionType: $modlogActionType, communityId: $communityId, userId: $userId, moderatorId: $moderatorId, commentId: $commentId, modlogEventItems: $modlogEventItems, hasReachedEnd: $hasReachedEnd, currentPage: $currentPage, message: $message)'; + } } /// @nodoc -abstract class $ModlogStateCopyWith<$Res> { - factory $ModlogStateCopyWith(ModlogState value, $Res Function(ModlogState) then) = _$ModlogStateCopyWithImpl<$Res, ModlogState>; +abstract mixin class $ModlogStateCopyWith<$Res> { + factory $ModlogStateCopyWith(ModlogState value, $Res Function(ModlogState) _then) = _$ModlogStateCopyWithImpl; @useResult $Res call( {ModlogStatus status, @@ -70,13 +95,11 @@ abstract class $ModlogStateCopyWith<$Res> { } /// @nodoc -class _$ModlogStateCopyWithImpl<$Res, $Val extends ModlogState> implements $ModlogStateCopyWith<$Res> { - _$ModlogStateCopyWithImpl(this._value, this._then); +class _$ModlogStateCopyWithImpl<$Res> implements $ModlogStateCopyWith<$Res> { + _$ModlogStateCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final ModlogState _self; + final $Res Function(ModlogState) _then; /// Create a copy of ModlogState /// with the given fields replaced by the non-null parameter values. @@ -94,138 +117,221 @@ class _$ModlogStateCopyWithImpl<$Res, $Val extends ModlogState> implements $Modl Object? currentPage = null, Object? message = freezed, }) { - return _then(_value.copyWith( + return _then(_self.copyWith( status: null == status - ? _value.status + ? _self.status : status // ignore: cast_nullable_to_non_nullable as ModlogStatus, modlogActionType: null == modlogActionType - ? _value.modlogActionType + ? _self.modlogActionType : modlogActionType // ignore: cast_nullable_to_non_nullable as ModlogActionType, communityId: freezed == communityId - ? _value.communityId + ? _self.communityId : communityId // ignore: cast_nullable_to_non_nullable as int?, userId: freezed == userId - ? _value.userId + ? _self.userId : userId // ignore: cast_nullable_to_non_nullable as int?, moderatorId: freezed == moderatorId - ? _value.moderatorId + ? _self.moderatorId : moderatorId // ignore: cast_nullable_to_non_nullable as int?, commentId: freezed == commentId - ? _value.commentId + ? _self.commentId : commentId // ignore: cast_nullable_to_non_nullable as int?, modlogEventItems: null == modlogEventItems - ? _value.modlogEventItems + ? _self.modlogEventItems : modlogEventItems // ignore: cast_nullable_to_non_nullable as List, hasReachedEnd: null == hasReachedEnd - ? _value.hasReachedEnd + ? _self.hasReachedEnd : hasReachedEnd // ignore: cast_nullable_to_non_nullable as bool, currentPage: null == currentPage - ? _value.currentPage + ? _self.currentPage : currentPage // ignore: cast_nullable_to_non_nullable as int, message: freezed == message - ? _value.message + ? _self.message : message // ignore: cast_nullable_to_non_nullable as String?, - ) as $Val); + )); } } -/// @nodoc -abstract class _$$ModlogStateImplCopyWith<$Res> implements $ModlogStateCopyWith<$Res> { - factory _$$ModlogStateImplCopyWith(_$ModlogStateImpl value, $Res Function(_$ModlogStateImpl) then) = __$$ModlogStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {ModlogStatus status, - ModlogActionType modlogActionType, - int? communityId, - int? userId, - int? moderatorId, - int? commentId, - List modlogEventItems, - bool hasReachedEnd, - int currentPage, - String? message}); -} +/// Adds pattern-matching-related methods to [ModlogState]. +extension ModlogStatePatterns on ModlogState { + /// A variant of `map` that fallback to returning `orElse`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeMap( + TResult Function(_ModlogState value)? $default, { + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case _ModlogState() when $default != null: + return $default(_that); + case _: + return orElse(); + } + } -/// @nodoc -class __$$ModlogStateImplCopyWithImpl<$Res> extends _$ModlogStateCopyWithImpl<$Res, _$ModlogStateImpl> implements _$$ModlogStateImplCopyWith<$Res> { - __$$ModlogStateImplCopyWithImpl(_$ModlogStateImpl _value, $Res Function(_$ModlogStateImpl) _then) : super(_value, _then); + /// A `switch`-like method, using callbacks. + /// + /// Callbacks receives the raw object, upcasted. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case final Subclass2 value: + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult map( + TResult Function(_ModlogState value) $default, + ) { + final _that = this; + switch (_that) { + case _ModlogState(): + return $default(_that); + case _: + throw StateError('Unexpected subclass'); + } + } - /// Create a copy of ModlogState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? status = null, - Object? modlogActionType = null, - Object? communityId = freezed, - Object? userId = freezed, - Object? moderatorId = freezed, - Object? commentId = freezed, - Object? modlogEventItems = null, - Object? hasReachedEnd = null, - Object? currentPage = null, - Object? message = freezed, + /// A variant of `map` that fallback to returning `null`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_ModlogState value)? $default, + ) { + final _that = this; + switch (_that) { + case _ModlogState() when $default != null: + return $default(_that); + case _: + return null; + } + } + + /// A variant of `when` that fallback to an `orElse` callback. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeWhen( + TResult Function(ModlogStatus status, ModlogActionType modlogActionType, int? communityId, int? userId, int? moderatorId, int? commentId, List modlogEventItems, + bool hasReachedEnd, int currentPage, String? message)? + $default, { + required TResult orElse(), }) { - return _then(_$ModlogStateImpl( - status: null == status - ? _value.status - : status // ignore: cast_nullable_to_non_nullable - as ModlogStatus, - modlogActionType: null == modlogActionType - ? _value.modlogActionType - : modlogActionType // ignore: cast_nullable_to_non_nullable - as ModlogActionType, - communityId: freezed == communityId - ? _value.communityId - : communityId // ignore: cast_nullable_to_non_nullable - as int?, - userId: freezed == userId - ? _value.userId - : userId // ignore: cast_nullable_to_non_nullable - as int?, - moderatorId: freezed == moderatorId - ? _value.moderatorId - : moderatorId // ignore: cast_nullable_to_non_nullable - as int?, - commentId: freezed == commentId - ? _value.commentId - : commentId // ignore: cast_nullable_to_non_nullable - as int?, - modlogEventItems: null == modlogEventItems - ? _value._modlogEventItems - : modlogEventItems // ignore: cast_nullable_to_non_nullable - as List, - hasReachedEnd: null == hasReachedEnd - ? _value.hasReachedEnd - : hasReachedEnd // ignore: cast_nullable_to_non_nullable - as bool, - currentPage: null == currentPage - ? _value.currentPage - : currentPage // ignore: cast_nullable_to_non_nullable - as int, - message: freezed == message - ? _value.message - : message // ignore: cast_nullable_to_non_nullable - as String?, - )); + final _that = this; + switch (_that) { + case _ModlogState() when $default != null: + return $default( + _that.status, _that.modlogActionType, _that.communityId, _that.userId, _that.moderatorId, _that.commentId, _that.modlogEventItems, _that.hasReachedEnd, _that.currentPage, _that.message); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// As opposed to `map`, this offers destructuring. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case Subclass2(:final field2): + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult when( + TResult Function(ModlogStatus status, ModlogActionType modlogActionType, int? communityId, int? userId, int? moderatorId, int? commentId, List modlogEventItems, + bool hasReachedEnd, int currentPage, String? message) + $default, + ) { + final _that = this; + switch (_that) { + case _ModlogState(): + return $default( + _that.status, _that.modlogActionType, _that.communityId, _that.userId, _that.moderatorId, _that.commentId, _that.modlogEventItems, _that.hasReachedEnd, _that.currentPage, _that.message); + case _: + throw StateError('Unexpected subclass'); + } + } + + /// A variant of `when` that fallback to returning `null` + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function(ModlogStatus status, ModlogActionType modlogActionType, int? communityId, int? userId, int? moderatorId, int? commentId, List modlogEventItems, + bool hasReachedEnd, int currentPage, String? message)? + $default, + ) { + final _that = this; + switch (_that) { + case _ModlogState() when $default != null: + return $default( + _that.status, _that.modlogActionType, _that.communityId, _that.userId, _that.moderatorId, _that.commentId, _that.modlogEventItems, _that.hasReachedEnd, _that.currentPage, _that.message); + case _: + return null; + } } } /// @nodoc -class _$ModlogStateImpl extends _ModlogState { - const _$ModlogStateImpl( +class _ModlogState extends ModlogState { + const _ModlogState( {this.status = ModlogStatus.initial, this.modlogActionType = ModlogActionType.all, this.communityId, @@ -291,16 +397,18 @@ class _$ModlogStateImpl extends _ModlogState { @override final String? message; + /// Create a copy of ModlogState + /// with the given fields replaced by the non-null parameter values. @override - String toString() { - return 'ModlogState(status: $status, modlogActionType: $modlogActionType, communityId: $communityId, userId: $userId, moderatorId: $moderatorId, commentId: $commentId, modlogEventItems: $modlogEventItems, hasReachedEnd: $hasReachedEnd, currentPage: $currentPage, message: $message)'; - } + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$ModlogStateCopyWith<_ModlogState> get copyWith => __$ModlogStateCopyWithImpl<_ModlogState>(this, _$identity); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$ModlogStateImpl && + other is _ModlogState && (identical(other.status, status) || other.status == status) && (identical(other.modlogActionType, modlogActionType) || other.modlogActionType == modlogActionType) && (identical(other.communityId, communityId) || other.communityId == communityId) && @@ -317,71 +425,96 @@ class _$ModlogStateImpl extends _ModlogState { int get hashCode => Object.hash(runtimeType, status, modlogActionType, communityId, userId, moderatorId, commentId, const DeepCollectionEquality().hash(_modlogEventItems), hasReachedEnd, currentPage, message); - /// Create a copy of ModlogState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) @override - @pragma('vm:prefer-inline') - _$$ModlogStateImplCopyWith<_$ModlogStateImpl> get copyWith => __$$ModlogStateImplCopyWithImpl<_$ModlogStateImpl>(this, _$identity); + String toString() { + return 'ModlogState(status: $status, modlogActionType: $modlogActionType, communityId: $communityId, userId: $userId, moderatorId: $moderatorId, commentId: $commentId, modlogEventItems: $modlogEventItems, hasReachedEnd: $hasReachedEnd, currentPage: $currentPage, message: $message)'; + } } -abstract class _ModlogState extends ModlogState { - const factory _ModlogState( - {final ModlogStatus status, - final ModlogActionType modlogActionType, - final int? communityId, - final int? userId, - final int? moderatorId, - final int? commentId, - final List modlogEventItems, - final bool hasReachedEnd, - final int currentPage, - final String? message}) = _$ModlogStateImpl; - const _ModlogState._() : super._(); - - /// The status of the modlog feed. - @override - ModlogStatus get status; - - /// The type of modlog action to filter by. - @override - ModlogActionType get modlogActionType; - - /// A community ID to filter the modlog by. - @override - int? get communityId; - - /// A user ID to filter the modlog by. - @override - int? get userId; - - /// A moderator ID to filter the modlog by. - @override - int? get moderatorId; - - /// A comment ID to filter the modlog by. - @override - int? get commentId; - - /// The list of modlog events. - @override - List get modlogEventItems; - - /// Whether the end of the modlog has been reached. +/// @nodoc +abstract mixin class _$ModlogStateCopyWith<$Res> implements $ModlogStateCopyWith<$Res> { + factory _$ModlogStateCopyWith(_ModlogState value, $Res Function(_ModlogState) _then) = __$ModlogStateCopyWithImpl; @override - bool get hasReachedEnd; + @useResult + $Res call( + {ModlogStatus status, + ModlogActionType modlogActionType, + int? communityId, + int? userId, + int? moderatorId, + int? commentId, + List modlogEventItems, + bool hasReachedEnd, + int currentPage, + String? message}); +} - /// The current page of the modlog. - @override - int get currentPage; +/// @nodoc +class __$ModlogStateCopyWithImpl<$Res> implements _$ModlogStateCopyWith<$Res> { + __$ModlogStateCopyWithImpl(this._self, this._then); - /// The error message to display after a failure. - @override - String? get message; + final _ModlogState _self; + final $Res Function(_ModlogState) _then; /// Create a copy of ModlogState /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$ModlogStateImplCopyWith<_$ModlogStateImpl> get copyWith => throw _privateConstructorUsedError; + @pragma('vm:prefer-inline') + $Res call({ + Object? status = null, + Object? modlogActionType = null, + Object? communityId = freezed, + Object? userId = freezed, + Object? moderatorId = freezed, + Object? commentId = freezed, + Object? modlogEventItems = null, + Object? hasReachedEnd = null, + Object? currentPage = null, + Object? message = freezed, + }) { + return _then(_ModlogState( + status: null == status + ? _self.status + : status // ignore: cast_nullable_to_non_nullable + as ModlogStatus, + modlogActionType: null == modlogActionType + ? _self.modlogActionType + : modlogActionType // ignore: cast_nullable_to_non_nullable + as ModlogActionType, + communityId: freezed == communityId + ? _self.communityId + : communityId // ignore: cast_nullable_to_non_nullable + as int?, + userId: freezed == userId + ? _self.userId + : userId // ignore: cast_nullable_to_non_nullable + as int?, + moderatorId: freezed == moderatorId + ? _self.moderatorId + : moderatorId // ignore: cast_nullable_to_non_nullable + as int?, + commentId: freezed == commentId + ? _self.commentId + : commentId // ignore: cast_nullable_to_non_nullable + as int?, + modlogEventItems: null == modlogEventItems + ? _self._modlogEventItems + : modlogEventItems // ignore: cast_nullable_to_non_nullable + as List, + hasReachedEnd: null == hasReachedEnd + ? _self.hasReachedEnd + : hasReachedEnd // ignore: cast_nullable_to_non_nullable + as bool, + currentPage: null == currentPage + ? _self.currentPage + : currentPage // ignore: cast_nullable_to_non_nullable + as int, + message: freezed == message + ? _self.message + : message // ignore: cast_nullable_to_non_nullable + as String?, + )); + } } + +// dart format on diff --git a/lib/src/features/modlog/presentation/state/modlog_state.dart b/lib/src/features/modlog/presentation/state/modlog_state.dart index 93fa9042e..bd4ec3592 100644 --- a/lib/src/features/modlog/presentation/state/modlog_state.dart +++ b/lib/src/features/modlog/presentation/state/modlog_state.dart @@ -3,7 +3,7 @@ part of 'modlog_cubit.dart'; enum ModlogStatus { initial, fetching, success, failure } @freezed -class ModlogState with _$ModlogState { +abstract class ModlogState with _$ModlogState { const factory ModlogState({ /// The status of the modlog feed. @Default(ModlogStatus.initial) ModlogStatus status, diff --git a/lib/src/features/modlog/presentation/widgets/modlog_feed_page_app_bar.dart b/lib/src/features/modlog/presentation/widgets/modlog_feed_page_app_bar.dart index e368b0a06..995c087bc 100644 --- a/lib/src/features/modlog/presentation/widgets/modlog_feed_page_app_bar.dart +++ b/lib/src/features/modlog/presentation/widgets/modlog_feed_page_app_bar.dart @@ -23,7 +23,7 @@ class ModlogFeedPageAppBar extends StatelessWidget { final theme = Theme.of(context); final l10n = AppLocalizations.of(context)!; - final hideTopBarOnScroll = context.select((bloc) => bloc.state.hideTopBarOnScroll); + final hideTopBarOnScroll = context.select((bloc) => bloc.state.hideTopBarOnScroll); return SliverAppBar( pinned: !hideTopBarOnScroll, diff --git a/lib/src/features/notification/presentation/utils/notification_server_utils.dart b/lib/src/features/notification/presentation/utils/notification_server_utils.dart index 26448ab31..fa8aa949a 100644 --- a/lib/src/features/notification/presentation/utils/notification_server_utils.dart +++ b/lib/src/features/notification/presentation/utils/notification_server_utils.dart @@ -77,13 +77,12 @@ Future deleteAccountFromNotificationServer() async { } } -Future requestTestNotification() async { +Future requestTestNotification(Account account) async { try { final prefs = UserPreferences.instance.preferences; String pushNotificationServer = prefs.getString(LocalSettings.pushNotificationServer.name) ?? THUNDER_SERVER_URL; final l10n = AppLocalizations.of(GlobalContext.context)!; - final account = await fetchActiveProfile(); if (account.anonymous) throw Exception(l10n.userNotLoggedIn); // Send POST request to notification server diff --git a/lib/src/features/post/post.dart b/lib/src/features/post/post.dart index f5aa7f70b..0fce15156 100644 --- a/lib/src/features/post/post.dart +++ b/lib/src/features/post/post.dart @@ -5,6 +5,7 @@ export 'domain/enums/enums.dart'; export 'presentation/pages/pages.dart'; export 'presentation/utils/post_media_utils.dart'; export 'presentation/utils/post_optimistic_utils.dart'; +export 'presentation/utils/post_list_action_controller.dart'; export 'presentation/utils/user_label_dialog_utils.dart'; export 'presentation/widgets/widgets.dart'; export 'package:thunder/src/foundation/primitives/models/thunder_post.dart'; diff --git a/lib/src/features/post/presentation/pages/create_post_page.dart b/lib/src/features/post/presentation/pages/create_post_page.dart index 5b381903d..9ecf3f0ad 100644 --- a/lib/src/features/post/presentation/pages/create_post_page.dart +++ b/lib/src/features/post/presentation/pages/create_post_page.dart @@ -22,6 +22,7 @@ import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/drafts/drafts.dart'; import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/features/search/search.dart'; +import 'package:thunder/src/features/session/session.dart'; import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; import 'package:thunder/src/features/post/presentation/widgets/cross_posts.dart'; @@ -99,12 +100,6 @@ class CreatePostPage extends StatefulWidget { } class _CreatePostPageState extends State with WidgetsBindingObserver { - /// The account to use for the post - Account? account; - - /// The account's user information - ThunderUser? user; - final DraftRepository _draftRepository = DraftRepositoryImpl(database: database); /// Whether to save this post as a draft @@ -167,13 +162,16 @@ class _CreatePostPageState extends State with WidgetsBindingObse bool userChanged = false; + /// The id of the account for which the draft is being saved. This is used to determine which draft to restore when the page is opened. + String? _draftAccountId; + @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); - account = widget.account ?? context.read().state.account; + _draftAccountId = widget.account?.id ?? context.read().state.effectiveAccount.id; communityId = widget.communityId; @@ -339,12 +337,14 @@ class _CreatePostPageState extends State with WidgetsBindingObse } Future _restoreCommunity() async { - if (community != null || communityId == null || account == null) { + final account = context.read().state.effectiveAccount; + + if (community != null || communityId == null) { return; } try { - final details = await CommunityRepositoryImpl(account: account!).getCommunity(id: communityId); + final details = await CommunityRepositoryImpl(account: account).getCommunity(id: communityId); if (!mounted) { return; @@ -365,7 +365,7 @@ class _CreatePostPageState extends State with WidgetsBindingObse Draft _buildDraft() => buildPostDraft( context: _draftContext, - accountId: account?.id, + accountId: _draftAccountId, title: _titleTextController.text, url: _urlTextController.text, customThumbnail: _customThumbnailTextController.text, @@ -425,401 +425,412 @@ class _CreatePostPageState extends State with WidgetsBindingObse return PopScope( onPopInvokedWithResult: (didPop, result) {}, - child: BlocConsumer( - listener: (context, state) { - if (state.status == CreatePostStatus.success && state.post != null) { - widget.onPostSuccess?.call(state.post!, userChanged); - Navigator.of(context).pop(); - } - - if (state.status == CreatePostStatus.error && state.message != null) { - showSnackbar(state.message!); - context.read().clearMessage(); - } - - switch (state.status) { - case CreatePostStatus.imageUploadSuccess: - String markdownImages = state.imageUrls?.map((url) => '![]($url)').join('\n\n') ?? ''; - _bodyTextController.text = _bodyTextController.text.replaceRange(_bodyTextController.selection.end, _bodyTextController.selection.end, markdownImages); - break; - case CreatePostStatus.postImageUploadSuccess: - _urlTextController.text = state.imageUrls?.first ?? ''; - break; - case CreatePostStatus.imageUploadFailure: - case CreatePostStatus.postImageUploadFailure: - showSnackbar( - l10n.postUploadImageError + (state.message?.isNotEmpty == true ? '. ${state.message}' : ''), - leadingIcon: Icons.warning_rounded, - leadingIconColor: theme.colorScheme.errorContainer, - ); - default: - break; - } + child: BlocConsumer( + listener: (context, featureAccountState) { + _draftAccountId = featureAccountState.effectiveAccount.id; + context.read().switchAccount(featureAccountState.effectiveAccount); }, - builder: (context, state) { - return GestureDetector( - onTap: () { - FocusManager.instance.primaryFocus?.unfocus(); + builder: (context, featureAccountState) { + final account = featureAccountState.effectiveAccount; + + return BlocConsumer( + listener: (context, state) { + if (state.status == CreatePostStatus.success && state.post != null) { + widget.onPostSuccess?.call(state.post!, userChanged); + Navigator.of(context).pop(); + } + + if (state.status == CreatePostStatus.error && state.message != null) { + showSnackbar(state.message!); + context.read().clearMessage(); + } + + switch (state.status) { + case CreatePostStatus.imageUploadSuccess: + String markdownImages = state.imageUrls?.map((url) => '![]($url)').join('\n\n') ?? ''; + _bodyTextController.text = _bodyTextController.text.replaceRange(_bodyTextController.selection.end, _bodyTextController.selection.end, markdownImages); + break; + case CreatePostStatus.postImageUploadSuccess: + _urlTextController.text = state.imageUrls?.first ?? ''; + break; + case CreatePostStatus.imageUploadFailure: + case CreatePostStatus.postImageUploadFailure: + showSnackbar( + l10n.postUploadImageError + (state.message?.isNotEmpty == true ? '. ${state.message}' : ''), + leadingIcon: Icons.warning_rounded, + leadingIconColor: theme.colorScheme.errorContainer, + ); + default: + break; + } }, - child: Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - title: Text(widget.post != null ? l10n.editPost : l10n.createPost), - centerTitle: false, - ), - body: SafeArea( - bottom: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 8.0), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - CommunitySelector( - account: account!, - community: community, - onCommunitySelected: (ThunderCommunity c) { - setState(() { - communityId = c.id; - community = c; - }); - _onDraftInputChanged(); - _validateSubmission(); - }, - ), - const SizedBox(height: 4.0), - UserSelector( - account: account!, - communityActorId: community?.actorId, - onCommunityChanged: (community) { - setState(() { - communityId = community?.id; - community = community; - }); - - _onDraftInputChanged(); - _validateSubmission(); - }, - onUserChanged: (account) { - setState(() { - userChanged = this.account?.instance != account.instance; - this.account = account; - }); - - context.read().switchAccount(account); - _onDraftInputChanged(); - }, - enableAccountSwitching: widget.post == null, - ), - const SizedBox(height: 12.0), - TypeAheadField( - controller: _titleTextController, - suggestionsCallback: (String pattern) async { - if (pattern.isEmpty) { - String? linkTitle = await _getDataFromLink(link: _urlTextController.text, updateTitleField: false); - if (linkTitle?.isNotEmpty == true) { - return [linkTitle!]; - } - } - return []; - }, - itemBuilder: (BuildContext context, String itemData) { - return ListTile( - title: Text(itemData), - subtitle: Text(l10n.suggestedTitle), - ); - }, - onSelected: (String suggestion) { - _titleTextController.text = suggestion; - }, - builder: (context, controller, focusNode) => TextField( - controller: controller, - focusNode: focusNode, - decoration: InputDecoration( - labelText: l10n.postTitle, - helperText: l10n.requiredField, - isDense: true, - border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.all(13), + builder: (context, state) { + return GestureDetector( + onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); + }, + child: Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: Text(widget.post != null ? l10n.editPost : l10n.createPost), + centerTitle: false, + ), + body: SafeArea( + bottom: false, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 8.0), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + CommunitySelector( + account: account, + community: community, + onCommunitySelected: (ThunderCommunity c) { + setState(() { + communityId = c.id; + community = c; + }); + _onDraftInputChanged(); + _validateSubmission(); + }, ), - ), - hideOnEmpty: true, - hideOnLoading: true, - hideOnError: true, - ), - const SizedBox(height: 10), - TextFormField( - controller: _urlTextController, - decoration: InputDecoration( - labelText: l10n.postURL, - errorText: urlError, - isDense: true, - border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.all(13), - suffixIcon: IconButton( - onPressed: () async { - if (state.status == CreatePostStatus.postImageUploadInProgress) { - return; - } - - List imagesPath = await selectImagesToUpload(); - if (context.mounted) { - context.read().uploadImages(imagesPath, isPostImage: true); - } + const SizedBox(height: 4.0), + UserSelector( + account: account, + communityActorId: community?.actorId, + onCommunityChanged: (community) { + setState(() { + communityId = community?.id; + community = community; + }); + + _onDraftInputChanged(); + _validateSubmission(); }, - icon: state.status == CreatePostStatus.postImageUploadInProgress - ? const SizedBox( - width: 20, - height: 20, - child: Center( - child: SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(), - ), - ), - ) - : Icon(Icons.image, semanticLabel: l10n.uploadImage), + onUserChanged: (account) { + setState(() { + userChanged = featureAccountState.effectiveAccount.id != account.id; + _draftAccountId = account.id; + }); + + context.read().setOverride(account); + _onDraftInputChanged(); + }, + enableAccountSwitching: widget.post == null, ), - ), - ), - if (!isImageUrl(_urlTextController.text)) ...[ - const SizedBox(height: 10), - TextFormField( - controller: _customThumbnailTextController, - decoration: InputDecoration( - labelText: l10n.thumbnailUrl, - errorText: customThumbnailError, - isDense: true, - border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.all(13), + const SizedBox(height: 12.0), + TypeAheadField( + controller: _titleTextController, + suggestionsCallback: (String pattern) async { + if (pattern.isEmpty) { + String? linkTitle = await _getDataFromLink(link: _urlTextController.text, updateTitleField: false); + if (linkTitle?.isNotEmpty == true) { + return [linkTitle!]; + } + } + return []; + }, + itemBuilder: (BuildContext context, String itemData) { + return ListTile( + title: Text(itemData), + subtitle: Text(l10n.suggestedTitle), + ); + }, + onSelected: (String suggestion) { + _titleTextController.text = suggestion; + }, + builder: (context, controller, focusNode) => TextField( + controller: controller, + focusNode: focusNode, + decoration: InputDecoration( + labelText: l10n.postTitle, + helperText: l10n.requiredField, + isDense: true, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.all(13), + ), + ), + hideOnEmpty: true, + hideOnLoading: true, + hideOnError: true, ), - ), - ], - if (isImageUrl(_urlTextController.text)) ...[ - const SizedBox(height: 10), - TextFormField( - controller: _altTextTextController, - decoration: InputDecoration( - labelText: l10n.altText, - isDense: true, - border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.all(13), + const SizedBox(height: 10), + TextFormField( + controller: _urlTextController, + decoration: InputDecoration( + labelText: l10n.postURL, + errorText: urlError, + isDense: true, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.all(13), + suffixIcon: IconButton( + onPressed: () async { + if (state.status == CreatePostStatus.postImageUploadInProgress) { + return; + } + + List imagesPath = await selectImagesToUpload(); + if (context.mounted) { + context.read().uploadImages(imagesPath, isPostImage: true); + } + }, + icon: state.status == CreatePostStatus.postImageUploadInProgress + ? const SizedBox( + width: 20, + height: 20, + child: Center( + child: SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(), + ), + ), + ) + : Icon(Icons.image, semanticLabel: l10n.uploadImage), + ), + ), ), - ), - ], - SizedBox(height: url.isNotEmpty ? 10 : 5), - Visibility( - visible: url.isNotEmpty, - child: MediaView( - showFullHeightImages: false, - edgeToEdgeImages: false, - viewMode: ViewMode.comfortable, - markPostReadOnMediaView: false, - isUserLoggedIn: true, - media: Media( - originalUrl: url, - mediaUrl: isImageUrl(url) - ? url - : customThumbnail?.isNotEmpty == true && isImageUrl(customThumbnail!) - ? customThumbnail - : null, - nsfw: isNSFW, - mediaType: MediaType.link, + if (!isImageUrl(_urlTextController.text)) ...[ + const SizedBox(height: 10), + TextFormField( + controller: _customThumbnailTextController, + decoration: InputDecoration( + labelText: l10n.thumbnailUrl, + errorText: customThumbnailError, + isDense: true, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.all(13), + ), + ), + ], + if (isImageUrl(_urlTextController.text)) ...[ + const SizedBox(height: 10), + TextFormField( + controller: _altTextTextController, + decoration: InputDecoration( + labelText: l10n.altText, + isDense: true, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.all(13), + ), + ), + ], + SizedBox(height: url.isNotEmpty ? 10 : 5), + Visibility( + visible: url.isNotEmpty, + child: MediaView( + showFullHeightImages: false, + edgeToEdgeImages: false, + viewMode: ViewMode.comfortable, + markPostReadOnMediaView: false, + isUserLoggedIn: true, + media: Media( + originalUrl: url, + mediaUrl: isImageUrl(url) + ? url + : customThumbnail?.isNotEmpty == true && isImageUrl(customThumbnail!) + ? customThumbnail + : null, + nsfw: isNSFW, + mediaType: MediaType.link, + ), + ), ), - ), - ), - if (crossPosts.isNotEmpty && widget.post == null) const SizedBox(height: 6), - Visibility( - visible: url.isNotEmpty && crossPosts.isNotEmpty, - child: CrossPosts( - crossPosts: crossPosts, - isNewPost: true, - ), - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ConstrainedBox( - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.60), - child: LanguageSelector( - languageId: languageId, - onLanguageSelected: (ThunderLanguage? language) { - setState(() => languageId = language?.id); - _onDraftInputChanged(); - }, + if (crossPosts.isNotEmpty && widget.post == null) const SizedBox(height: 6), + Visibility( + visible: url.isNotEmpty && crossPosts.isNotEmpty, + child: CrossPosts( + crossPosts: crossPosts, + isNewPost: true, ), ), - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text(l10n.nsfw), - const SizedBox(width: 4.0), - Switch( - value: isNSFW, - onChanged: (bool value) { - setState(() => isNSFW = value); - _onDraftInputChanged(); - }, + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.60), + child: LanguageSelector( + account: account, + languageId: languageId, + onLanguageSelected: (ThunderLanguage? language) { + setState(() => languageId = language?.id); + _onDraftInputChanged(); + }, + ), + ), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text(l10n.nsfw), + const SizedBox(width: 4.0), + Switch( + value: isNSFW, + onChanged: (bool value) { + setState(() => isNSFW = value); + _onDraftInputChanged(); + }, + ), + ], ), ], ), - ], - ), - const SizedBox(height: 10), - AnimatedCrossFade( - firstChild: Container( - margin: const EdgeInsets.only(top: 8.0), - width: double.infinity, - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: getBackgroundColor(context), - borderRadius: const BorderRadius.all(Radius.circular(8.0)), + const SizedBox(height: 10), + AnimatedCrossFade( + firstChild: Container( + margin: const EdgeInsets.only(top: 8.0), + width: double.infinity, + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: getBackgroundColor(context), + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + ), + child: CommonMarkdownBody(body: _bodyTextController.text, isComment: true, nsfw: isNSFW && hideNsfwPreviews), + ), + secondChild: MarkdownTextInputField( + controller: _bodyTextController, + focusNode: _bodyFocusNode, + label: l10n.postBody, + minLines: 8, + maxLines: null, + textStyle: theme.textTheme.bodyLarge, + spellCheckConfiguration: const SpellCheckConfiguration.disabled(), + ), + crossFadeState: showPreview ? CrossFadeState.showFirst : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 120), + excludeBottomFocus: false, ), - child: CommonMarkdownBody(body: _bodyTextController.text, isComment: true, nsfw: isNSFW && hideNsfwPreviews), - ), - secondChild: MarkdownTextInputField( - controller: _bodyTextController, - focusNode: _bodyFocusNode, - label: l10n.postBody, - minLines: 8, - maxLines: null, - textStyle: theme.textTheme.bodyLarge, - spellCheckConfiguration: const SpellCheckConfiguration.disabled(), - ), - crossFadeState: showPreview ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 120), - excludeBottomFocus: false, + ]), ), - ]), + ), ), - ), - ), - const Divider(height: 1), - Container( - color: theme.cardColor, - margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - child: Row( - children: [ - Expanded( - child: MarkdownToolbar( - controller: _bodyTextController, - focusNode: _bodyFocusNode, - actions: const [ - MarkdownType.image, - MarkdownType.link, - MarkdownType.bold, - MarkdownType.italic, - MarkdownType.blockquote, - MarkdownType.strikethrough, - MarkdownType.title, - MarkdownType.list, - MarkdownType.separator, - MarkdownType.code, - MarkdownType.spoiler, - MarkdownType.username, - MarkdownType.community, - ], - customTapActions: { - MarkdownType.username: () { - showUserInputDialog( - context, - title: l10n.username, - account: account!, - onUserSelected: (ThunderUser user) { - _bodyTextController.text = _bodyTextController.text.replaceRange( - _bodyTextController.selection.end, - _bodyTextController.selection.end, - '[@${user.name}@${fetchInstanceNameFromUrl(user.actorId)}](${user.actorId})', + const Divider(height: 1), + Container( + color: theme.cardColor, + margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Row( + children: [ + Expanded( + child: MarkdownToolbar( + controller: _bodyTextController, + focusNode: _bodyFocusNode, + actions: const [ + MarkdownType.image, + MarkdownType.link, + MarkdownType.bold, + MarkdownType.italic, + MarkdownType.blockquote, + MarkdownType.strikethrough, + MarkdownType.title, + MarkdownType.list, + MarkdownType.separator, + MarkdownType.code, + MarkdownType.spoiler, + MarkdownType.username, + MarkdownType.community, + ], + customTapActions: { + MarkdownType.username: () { + showUserInputDialog( + context, + title: l10n.username, + account: account, + onUserSelected: (ThunderUser user) { + _bodyTextController.text = _bodyTextController.text.replaceRange( + _bodyTextController.selection.end, + _bodyTextController.selection.end, + '[@${user.name}@${fetchInstanceNameFromUrl(user.actorId)}](${user.actorId})', + ); + }, ); }, - ); - }, - MarkdownType.community: () { - showCommunityInputDialog( - context, - title: l10n.community, - account: account!, - onCommunitySelected: (community) { - _bodyTextController.text = _bodyTextController.text - .replaceRange(_bodyTextController.selection.end, _bodyTextController.selection.end, '!${community.name}@${fetchInstanceNameFromUrl(community.actorId)}'); + MarkdownType.community: () { + showCommunityInputDialog( + context, + title: l10n.community, + account: account, + onCommunitySelected: (community) { + _bodyTextController.text = _bodyTextController.text + .replaceRange(_bodyTextController.selection.end, _bodyTextController.selection.end, '!${community.name}@${fetchInstanceNameFromUrl(community.actorId)}'); + }, + ); }, - ); - }, - }, - imageIsLoading: state.status == CreatePostStatus.imageUploadInProgress, - customImageButtonAction: () async { - if (state.status == CreatePostStatus.imageUploadInProgress) { - return; - } - - List imagesPath = await selectImagesToUpload(allowMultiple: true); - if (context.mounted) { - context.read().uploadImages(imagesPath, isPostImage: false); - } - }, - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 2.0, top: 2.0, left: 4.0, right: 2.0), - child: IconButton( - onPressed: () { - if (!showPreview) { - setState(() => wasKeyboardVisible = keyboardVisibilityController.isVisible); - FocusManager.instance.primaryFocus?.unfocus(); - } - - setState(() => showPreview = !showPreview); - if (!showPreview && wasKeyboardVisible) { - _bodyFocusNode.requestFocus(); - } - }, - icon: Icon( - showPreview ? Icons.visibility_off_rounded : Icons.visibility, - color: theme.colorScheme.onSecondary, - semanticLabel: l10n.postTogglePreview, + }, + imageIsLoading: state.status == CreatePostStatus.imageUploadInProgress, + customImageButtonAction: () async { + if (state.status == CreatePostStatus.imageUploadInProgress) { + return; + } + + List imagesPath = await selectImagesToUpload(allowMultiple: true); + if (context.mounted) { + context.read().uploadImages(imagesPath, isPostImage: false); + } + }, + ), ), - style: ElevatedButton.styleFrom(backgroundColor: theme.colorScheme.secondaryContainer), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 2.0, top: 2.0, left: 2.0, right: 8.0), - child: SizedBox( - width: 60, - child: IconButton( - onPressed: isSubmitButtonDisabled || state.status == CreatePostStatus.submitting ? null : () => _onCreatePost(context), - icon: state.status == CreatePostStatus.submitting - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(), - ) - : Icon( - widget.post != null ? Icons.edit_rounded : Icons.send_rounded, - color: theme.colorScheme.onSecondary, - semanticLabel: widget.post != null ? l10n.editPost : l10n.createPost, - ), - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.secondary, - disabledBackgroundColor: getBackgroundColor(context), + Padding( + padding: const EdgeInsets.only(bottom: 2.0, top: 2.0, left: 4.0, right: 2.0), + child: IconButton( + onPressed: () { + if (!showPreview) { + setState(() => wasKeyboardVisible = keyboardVisibilityController.isVisible); + FocusManager.instance.primaryFocus?.unfocus(); + } + + setState(() => showPreview = !showPreview); + if (!showPreview && wasKeyboardVisible) { + _bodyFocusNode.requestFocus(); + } + }, + icon: Icon( + showPreview ? Icons.visibility_off_rounded : Icons.visibility, + color: theme.colorScheme.onSecondary, + semanticLabel: l10n.postTogglePreview, + ), + style: ElevatedButton.styleFrom(backgroundColor: theme.colorScheme.secondaryContainer), ), ), - ), + Padding( + padding: const EdgeInsets.only(bottom: 2.0, top: 2.0, left: 2.0, right: 8.0), + child: SizedBox( + width: 60, + child: IconButton( + onPressed: isSubmitButtonDisabled || state.status == CreatePostStatus.submitting ? null : () => _onCreatePost(context), + icon: state.status == CreatePostStatus.submitting + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), + ) + : Icon( + widget.post != null ? Icons.edit_rounded : Icons.send_rounded, + color: theme.colorScheme.onSecondary, + semanticLabel: widget.post != null ? l10n.editPost : l10n.createPost, + ), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.secondary, + disabledBackgroundColor: getBackgroundColor(context), + ), + ), + ), + ), + ], ), - ], - ), - ), - Container( - height: MediaQuery.of(context).padding.bottom, - color: theme.cardColor, + ), + Container( + height: MediaQuery.of(context).padding.bottom, + color: theme.cardColor, + ), + ], ), - ], + ), ), - ), - ), + ); + }, ); }, ), @@ -831,7 +842,7 @@ class _CreatePostPageState extends State with WidgetsBindingObse try { // Fetch cross-posts - final response = await SearchRepositoryImpl(account: account!).search( + final response = await SearchRepositoryImpl(account: context.read().state.effectiveAccount).search( query: url, type: MetaSearchType.url, sort: SearchSortType.topAll, diff --git a/lib/src/features/post/presentation/pages/post_page.dart b/lib/src/features/post/presentation/pages/post_page.dart index afa47dbbc..d88818c9b 100644 --- a/lib/src/features/post/presentation/pages/post_page.dart +++ b/lib/src/features/post/presentation/pages/post_page.dart @@ -8,8 +8,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; +import 'package:thunder/src/app/shell/state/shell_chrome_cubit.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/account/account.dart'; +import 'package:thunder/src/features/account/data/cache/profile_site_info_cache.dart'; import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/src/shared/error_message.dart'; @@ -20,7 +21,6 @@ import 'package:thunder/src/features/post/presentation/widgets/cross_posts.dart' import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/shared/widgets/text/selectable_text_modal.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; -import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/packages/ui/ui.dart' show showSnackbar; @@ -74,10 +74,13 @@ class _PostPageState extends State { /// The timer for detecting when scrolling has stopped Timer? _updateScrollPositionTimer; + Set _blockedCommunityIds = const {}; + final Set _notifiedBlockedPostIds = {}; @override void initState() { super.initState(); + _loadBlockedCommunities(); highlightedCommentId = widget.highlightedCommentId; @@ -118,15 +121,34 @@ class _PostPageState extends State { }); } - bool listenWhen(PostState previous, PostState current) { - final l10n = GlobalContext.l10n; + Future _loadBlockedCommunities() async { + final account = context.read().account; + if (account.anonymous) return; - if (previous.status == PostStatus.loading && current.status == PostStatus.success && current.post != null && current.hasReachedCommentEnd) { - // Check if the post's community is blocked by the user. If so, show a message. - final blockedCommunities = context.read().state.siteResponse?.myUser?.communityBlocks; - final isCommunityBlocked = blockedCommunities?.any((c) => c.id == current.post?.community?.id) ?? false; + final siteInfo = await ProfileSiteInfoCache.instance.get(account); + if (!mounted) return; + + final blockedCommunityIds = siteInfo.myUser?.communityBlocks.map((community) => community.id).toSet() ?? {}; + setState(() => _blockedCommunityIds = blockedCommunityIds); + + final state = context.read().state; + if (state.status == PostStatus.success && state.post != null && state.hasReachedCommentEnd) { + _maybeShowBlockedCommunityMessage(state.post!); + } + } - if (isCommunityBlocked) showSnackbar(l10n.noVisibleComments); + void _maybeShowBlockedCommunityMessage(ThunderPost post) { + final communityId = post.community?.id; + if (communityId == null || !_blockedCommunityIds.contains(communityId) || !_notifiedBlockedPostIds.add(post.id)) { + return; + } + + showSnackbar(GlobalContext.l10n.noVisibleComments); + } + + bool listenWhen(PostState previous, PostState current) { + if (previous.status == PostStatus.loading && current.status == PostStatus.success && current.post != null && current.hasReachedCommentEnd) { + _maybeShowBlockedCommunityMessage(current.post!); } return true; @@ -155,7 +177,7 @@ class _PostPageState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); final l10n = AppLocalizations.of(context)!; - final thunderState = context.read().state; + final thunderState = context.read().state; final account = context.read().account; @@ -374,9 +396,9 @@ class _PostPageState extends State { if (thunderState.hideTopBarOnScroll) Positioned(child: Container(height: MediaQuery.of(context).padding.top, color: theme.colorScheme.surface)), AnimatedSwitcher( duration: const Duration(milliseconds: 200), - child: context.select((cubit) => cubit.state.isPostFabOpen) + child: context.select((cubit) => cubit.state.isPostFabOpen) ? Listener( - onPointerUp: (details) => context.read().setPostFabOpen(false), + onPointerUp: (details) => context.read().setPostFabOpen(false), child: Container(color: theme.colorScheme.surface.withValues(alpha: 0.95)), ) : null, diff --git a/lib/src/features/post/presentation/utils/post_list_action_controller.dart b/lib/src/features/post/presentation/utils/post_list_action_controller.dart new file mode 100644 index 000000000..dd8c07896 --- /dev/null +++ b/lib/src/features/post/presentation/utils/post_list_action_controller.dart @@ -0,0 +1,99 @@ +import 'package:thunder/src/features/post/post.dart'; + +class PostListActionController { + const PostListActionController({required PostRepository postRepository}) : _postRepository = postRepository; + + final PostRepository _postRepository; + + List reconcile({ + required List sourcePosts, + required List currentPosts, + }) { + if (currentPosts.isEmpty) return sourcePosts; + + final currentById = {for (final post in currentPosts) post.id: post}; + return sourcePosts.map((post) => currentById[post.id] ?? post).toList(growable: false); + } + + List updatePost(List posts, ThunderPost updatedPost) { + return posts.map((post) { + if (post.id != updatedPost.id) return post; + final preserveMedia = updatedPost.media.isEmpty && post.media.isNotEmpty; + return preserveMedia ? updatedPost.copyWith(media: post.media) : updatedPost; + }).toList(growable: false); + } + + List dismissHiddenPost(List posts, int postId) { + return posts.where((post) => post.id != postId).toList(growable: false); + } + + List dismissBlocked(List posts, {int? userId, int? communityId}) { + return posts.where((post) { + if (userId != null && post.creator?.id == userId) return false; + if (communityId != null && post.community?.id == communityId) return false; + return true; + }).toList(growable: false); + } + + Future> vote(List posts, ThunderPost post, int value) async { + final optimisticPosts = updatePost(posts, optimisticallyVotePost(post, value)); + + try { + final updatedPost = await _postRepository.vote(post, value); + return updatePost(optimisticPosts, updatedPost); + } catch (_) { + return posts; + } + } + + Future> save(List posts, ThunderPost post, bool value) async { + final optimisticPosts = updatePost(posts, optimisticallySavePost(post, value)); + + try { + final updatedPost = await _postRepository.save(post, value); + return updatePost(optimisticPosts, updatedPost); + } catch (_) { + return posts; + } + } + + Future> read(List posts, ThunderPost post, bool value) async { + final optimisticPosts = updatePost(posts, optimisticallyReadPost(post, value)); + + try { + final success = await _postRepository.read(post.id, value); + return success ? optimisticPosts : posts; + } catch (_) { + return posts; + } + } + + Future> multiRead(List posts, List postIds, bool value) async { + if (postIds.isEmpty) return posts; + + final targetIds = postIds.toSet(); + final originalById = {for (final post in posts.where((post) => targetIds.contains(post.id))) post.id: post}; + final optimisticPosts = posts.map((post) => targetIds.contains(post.id) ? optimisticallyReadPost(post, value) : post).toList(growable: false); + + try { + final failedIndexes = await _postRepository.readMultiple(postIds, value); + if (failedIndexes.isEmpty) return optimisticPosts; + + final failedIds = failedIndexes.where((index) => index >= 0 && index < postIds.length).map((index) => postIds[index]).toSet(); + return optimisticPosts.map((post) => failedIds.contains(post.id) ? (originalById[post.id] ?? post) : post).toList(growable: false); + } catch (_) { + return posts; + } + } + + Future> hide(List posts, ThunderPost post, bool value) async { + final optimisticPosts = updatePost(posts, optimisticallyHidePost(post, value)); + + try { + final success = await _postRepository.hide(post.id, value); + return success ? optimisticPosts : posts; + } catch (_) { + return posts; + } + } +} diff --git a/lib/src/features/post/presentation/widgets/post_body/post_body.dart b/lib/src/features/post/presentation/widgets/post_body/post_body.dart index 89ca6f623..e89019263 100644 --- a/lib/src/features/post/presentation/widgets/post_body/post_body.dart +++ b/lib/src/features/post/presentation/widgets/post_body/post_body.dart @@ -10,7 +10,6 @@ import 'package:expandable/expandable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; // Project imports -import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/shared/theme/color_utils.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; @@ -114,7 +113,7 @@ class _PostBodyState extends State with SingleTickerProviderStateMixin } void _updateIsOwnPost() { - final userId = context.read().state.account.userId; + final userId = context.read().account.userId; isOwnPost = widget.post.creator?.id == userId; } @@ -255,7 +254,7 @@ class _PostBodyState extends State with SingleTickerProviderStateMixin switch (postAction) { case PostAction.hide: - context.read().dismissHiddenPost(post!.id); + FeedActionScope.maybeOf(context)?.dismissHiddenPost(post!.id); break; default: break; @@ -263,7 +262,7 @@ class _PostBodyState extends State with SingleTickerProviderStateMixin switch (userAction) { case UserAction.block: - context.read().dismissBlocked(userId: post!.creator!.id); + FeedActionScope.maybeOf(context)?.dismissBlocked(userId: post!.creator!.id); break; default: break; @@ -271,7 +270,7 @@ class _PostBodyState extends State with SingleTickerProviderStateMixin switch (communityAction) { case CommunityAction.block: - context.read().dismissBlocked(communityId: post!.community!.id); + FeedActionScope.maybeOf(context)?.dismissBlocked(communityId: post!.community!.id); break; default: break; diff --git a/lib/src/features/post/presentation/widgets/post_body/post_body_action_bar.dart b/lib/src/features/post/presentation/widgets/post_body/post_body_action_bar.dart index 5e9388c1a..60782602d 100644 --- a/lib/src/features/post/presentation/widgets/post_body/post_body_action_bar.dart +++ b/lib/src/features/post/presentation/widgets/post_body/post_body_action_bar.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:thunder/src/features/account/data/cache/profile_site_info_cache.dart'; import 'package:thunder/src/features/account/account.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; @@ -11,7 +13,7 @@ import 'package:thunder/src/foundation/utils/utils.dart'; import 'package:thunder/packages/ui/ui.dart' show showSnackbar; /// A widget that displays the quick actions bar for a post -class PostBodyActionsBar extends StatelessWidget { +class PostBodyActionsBar extends StatefulWidget { const PostBodyActionsBar({ super.key, this.vote, @@ -60,126 +62,163 @@ class PostBodyActionsBar extends StatelessWidget { /// Called when the user wants to edit the post final Function()? onEdit; + @override + State createState() => _PostBodyActionsBarState(); +} + +class _PostBodyActionsBarState extends State { + Account? _account; + bool _downvotesEnabled = true; + bool _showScores = true; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final account = resolveEffectiveAccount(context); + if (_account?.id == account.id && _account?.instance == account.instance && _account?.anonymous == account.anonymous) { + return; + } + + _account = account; + _loadAccountDisplayPreferences(account); + } + + Future _loadAccountDisplayPreferences(Account account) async { + if (account.anonymous) { + if (!mounted) return; + setState(() { + _downvotesEnabled = true; + _showScores = true; + }); + return; + } + + final siteInfo = await ProfileSiteInfoCache.instance.get(account); + if (!mounted || _account?.id != account.id) return; + + setState(() { + _downvotesEnabled = siteInfo.site.enableDownvotes ?? true; + _showScores = siteInfo.myUser?.localUserView.localUser.showScores ?? true; + }); + } + @override Widget build(BuildContext context) { + final account = _account ?? resolveEffectiveAccount(context); final upvoteColor = context.select((cubit) => cubit.state.upvoteColor); final downvoteColor = context.select((cubit) => cubit.state.downvoteColor); final saveColor = context.select((cubit) => cubit.state.saveColor); final theme = Theme.of(context); final l10n = GlobalContext.l10n; - - return BlocSelector( - selector: (state) => state.isLoggedIn, - builder: (context, isUserLoggedIn) { - final downvotesEnabled = context.select((bloc) => bloc.state.downvotesEnabled); - final showScores = context.select((ProfileBloc bloc) => bloc.state.siteResponse?.myUser?.localUserView.localUser.showScores) ?? true; - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: TextButton( - onPressed: isUserLoggedIn ? () => onVote?.call(vote == 1 ? 0 : 1) : null, - style: TextButton.styleFrom( - fixedSize: const Size.fromHeight(40), - foregroundColor: vote == 1 ? theme.textTheme.bodyMedium?.color : context.read().state.upvoteColor.color, - padding: EdgeInsets.zero, + final isUserLoggedIn = !account.anonymous; + final downvotesEnabled = _downvotesEnabled; + final showScores = _showScores; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: TextButton( + onPressed: isUserLoggedIn ? () => widget.onVote?.call(widget.vote == 1 ? 0 : 1) : null, + style: TextButton.styleFrom( + fixedSize: const Size.fromHeight(40), + foregroundColor: widget.vote == 1 ? theme.textTheme.bodyMedium?.color : context.read().state.upvoteColor.color, + padding: EdgeInsets.zero, + ), + child: Wrap( + spacing: 4.0, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Icon( + Icons.arrow_upward_rounded, + semanticLabel: widget.vote == 1 ? l10n.upvoted : l10n.upvote, + color: isUserLoggedIn ? (widget.vote == 1 ? upvoteColor.color : theme.textTheme.bodyMedium?.color) : null, + size: 24.0, ), - child: Wrap( - spacing: 4.0, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Icon( - Icons.arrow_upward_rounded, - semanticLabel: vote == 1 ? l10n.upvoted : l10n.upvote, - color: isUserLoggedIn ? (vote == 1 ? upvoteColor.color : theme.textTheme.bodyMedium?.color) : null, - size: 24.0, + if (showScores) + Text( + formatNumberToK(widget.upvotes ?? 0), + style: TextStyle( + color: isUserLoggedIn ? (widget.vote == 1 ? upvoteColor.color : theme.textTheme.bodyMedium?.color) : null, ), - if (showScores) - Text( - formatNumberToK(upvotes ?? 0), - style: TextStyle( - color: isUserLoggedIn ? (vote == 1 ? upvoteColor.color : theme.textTheme.bodyMedium?.color) : null, - ), - ), - ], - ), - ), + ), + ], ), - if (downvotesEnabled) - Expanded( - child: TextButton( - onPressed: isUserLoggedIn ? () => onVote?.call(vote == -1 ? 0 : -1) : null, - style: TextButton.styleFrom( - fixedSize: const Size.fromHeight(40), - foregroundColor: vote == -1 ? theme.textTheme.bodyMedium?.color : downvoteColor.color, - padding: EdgeInsets.zero, + ), + ), + if (downvotesEnabled) + Expanded( + child: TextButton( + onPressed: isUserLoggedIn ? () => widget.onVote?.call(widget.vote == -1 ? 0 : -1) : null, + style: TextButton.styleFrom( + fixedSize: const Size.fromHeight(40), + foregroundColor: widget.vote == -1 ? theme.textTheme.bodyMedium?.color : downvoteColor.color, + padding: EdgeInsets.zero, + ), + child: Wrap( + spacing: 4.0, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Icon( + Icons.arrow_downward_rounded, + semanticLabel: widget.vote == -1 ? l10n.downvoted : l10n.downvote, + color: isUserLoggedIn ? (widget.vote == -1 ? downvoteColor.color : theme.textTheme.bodyMedium?.color) : null, + size: 24.0, ), - child: Wrap( - spacing: 4.0, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Icon( - Icons.arrow_downward_rounded, - semanticLabel: vote == -1 ? l10n.downvoted : l10n.downvote, - color: isUserLoggedIn ? (vote == -1 ? downvoteColor.color : theme.textTheme.bodyMedium?.color) : null, - size: 24.0, + if (showScores) + Text( + formatNumberToK(widget.downvotes ?? 0), + style: TextStyle( + color: isUserLoggedIn ? (widget.vote == -1 ? downvoteColor.color : theme.textTheme.bodyMedium?.color) : null, ), - if (showScores) - Text( - formatNumberToK(downvotes ?? 0), - style: TextStyle( - color: isUserLoggedIn ? (vote == -1 ? downvoteColor.color : theme.textTheme.bodyMedium?.color) : null, - ), - ), - ], - ), - ), - ), - Expanded( - child: IconButton( - onPressed: isUserLoggedIn ? () => onSave?.call(!saved) : null, - style: IconButton.styleFrom(foregroundColor: saved ? null : saveColor.color), - icon: Icon( - saved ? Icons.star_rounded : Icons.star_border_rounded, - semanticLabel: saved ? l10n.saved : l10n.save, - color: isUserLoggedIn ? (saved ? saveColor.color : theme.textTheme.bodyMedium?.color) : null, - ), + ), + ], ), ), - if (locked) - Expanded( - child: IconButton( - onPressed: () => showSnackbar(l10n.postLocked), - icon: Icon(Icons.lock, semanticLabel: l10n.postLocked, color: theme.colorScheme.error), - ), - ), - if (!locked && isOwnPost) - Expanded( - child: IconButton( - onPressed: isUserLoggedIn ? () => onEdit?.call() : null, - icon: Icon(Icons.edit_rounded, semanticLabel: l10n.edit), - ), - ), - if (!locked && !isOwnPost) - Expanded( - child: IconButton( - onPressed: isUserLoggedIn ? () => onReply?.call() : null, - icon: Icon(Icons.reply_rounded, semanticLabel: l10n.reply(0)), - ), - ), - Expanded( - child: IconButton( - onPressed: () => onShare?.call(), - icon: Icon(Icons.share_rounded, semanticLabel: l10n.share), - ), + ), + Expanded( + child: IconButton( + onPressed: isUserLoggedIn ? () => widget.onSave?.call(!widget.saved) : null, + style: IconButton.styleFrom(foregroundColor: widget.saved ? null : saveColor.color), + icon: Icon( + widget.saved ? Icons.star_rounded : Icons.star_border_rounded, + semanticLabel: widget.saved ? l10n.saved : l10n.save, + color: isUserLoggedIn ? (widget.saved ? saveColor.color : theme.textTheme.bodyMedium?.color) : null, + ), + ), + ), + if (widget.locked) + Expanded( + child: IconButton( + onPressed: () => showSnackbar(l10n.postLocked), + icon: Icon(Icons.lock, semanticLabel: l10n.postLocked, color: theme.colorScheme.error), + ), + ), + if (!widget.locked && widget.isOwnPost) + Expanded( + child: IconButton( + onPressed: isUserLoggedIn ? () => widget.onEdit?.call() : null, + icon: Icon(Icons.edit_rounded, semanticLabel: l10n.edit), + ), + ), + if (!widget.locked && !widget.isOwnPost) + Expanded( + child: IconButton( + onPressed: isUserLoggedIn ? () => widget.onReply?.call() : null, + icon: Icon(Icons.reply_rounded, semanticLabel: l10n.reply(0)), ), - ], + ), + Expanded( + child: IconButton( + onPressed: () => widget.onShare?.call(), + icon: Icon(Icons.share_rounded, semanticLabel: l10n.share), + ), ), - ); - }, + ], + ), ); } } diff --git a/lib/src/features/post/presentation/widgets/post_body/post_body_title.dart b/lib/src/features/post/presentation/widgets/post_body/post_body_title.dart index fcb3aa35d..934faeaa8 100644 --- a/lib/src/features/post/presentation/widgets/post_body/post_body_title.dart +++ b/lib/src/features/post/presentation/widgets/post_body/post_body_title.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; import 'package:thunder/src/shared/widgets/chips/community_chip.dart'; @@ -169,12 +169,12 @@ class _PostBodyAuthorCommunityMetadataState extends State().state; + final account = resolveEffectiveAccount(context); if (widget.post.creator?.botAccount == true) userGroups.add(UserType.bot); if (widget.post.creatorIsModerator ?? false) userGroups.add(UserType.moderator); if (widget.post.creatorIsAdmin ?? false) userGroups.add(UserType.admin); - if (widget.post.creator?.id == profileState.account.userId) userGroups.add(UserType.self); + if (widget.post.creator?.id == account.userId) userGroups.add(UserType.self); if (widget.post.creator?.published.month == DateTime.now().month && widget.post.creator?.published.day == DateTime.now().day) userGroups.add(UserType.birthday); } diff --git a/lib/src/features/post/presentation/widgets/post_bottom_sheet/community_post_action_bottom_sheet.dart b/lib/src/features/post/presentation/widgets/post_bottom_sheet/community_post_action_bottom_sheet.dart index 84faca5a4..a183f4fdd 100644 --- a/lib/src/features/post/presentation/widgets/post_bottom_sheet/community_post_action_bottom_sheet.dart +++ b/lib/src/features/post/presentation/widgets/post_bottom_sheet/community_post_action_bottom_sheet.dart @@ -65,6 +65,7 @@ enum CommunityPostAction { class CommunityPostActionBottomSheet extends StatefulWidget { const CommunityPostActionBottomSheet({ super.key, + required this.context, required this.post, required this.account, required this.moderatedCommunities, @@ -73,6 +74,9 @@ class CommunityPostActionBottomSheet extends StatefulWidget { required this.onAction, }); + /// The post information + final BuildContext context; + /// The post information final ThunderPost post; @@ -103,7 +107,11 @@ class _CommunityPostActionBottomSheetState extends State PostActionBottomSheet(context: context, initialPage: page, post: post, onAction: onAction), + builder: (_) => wrapWithCapturedAccountContext(context, PostActionBottomSheet(context: context, initialPage: page, post: post, onAction: onAction)), ); } @@ -87,7 +87,7 @@ class _PostActionBottomSheetState extends State { @override void initState() { super.initState(); - account = context.read().state.account; + account = resolveEffectiveAccount(widget.context); currentPage = widget.initialPage; BackButtonInterceptor.add(_handleBack); @@ -173,6 +173,7 @@ class _PostActionBottomSheetState extends State { }, ), GeneralPostAction.community => CommunityPostActionBottomSheet( + context: widget.context, account: account, post: widget.post, moderatedCommunities: moderatedCommunities, @@ -190,6 +191,7 @@ class _PostActionBottomSheetState extends State { }, ), GeneralPostAction.instance => InstanceActionBottomSheet( + context: widget.context, account: account, blockedInstances: blockedInstances, userInstanceId: widget.post.creator?.instanceId, @@ -236,7 +238,7 @@ class _PostActionBottomSheetState extends State { if (currentPage == GeneralPostAction.general) Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), - child: LanguagePostCardMetaData(languageId: widget.post.languageId), + child: LanguagePostCardMetaData(languageId: widget.post.languageId, account: account), ), const SizedBox(height: 16.0), actions, diff --git a/lib/src/features/post/presentation/widgets/post_bottom_sheet/post_post_action_bottom_sheet.dart b/lib/src/features/post/presentation/widgets/post_bottom_sheet/post_post_action_bottom_sheet.dart index 75044a3e6..883d6fc68 100644 --- a/lib/src/features/post/presentation/widgets/post_bottom_sheet/post_post_action_bottom_sheet.dart +++ b/lib/src/features/post/presentation/widgets/post_bottom_sheet/post_post_action_bottom_sheet.dart @@ -131,7 +131,7 @@ class _PostPostActionBottomSheetState extends State { return; case PostPostAction.editPost: Navigator.of(context).pop(); - navigateToCreatePostPage(context, communityId: widget.post.community?.id, post: widget.post); + navigateToCreatePostPage(widget.context, account: widget.account, communityId: widget.post.community?.id, post: widget.post); return; case PostPostAction.deletePost: Navigator.of(context).pop(); diff --git a/lib/src/features/post/presentation/widgets/post_page_app_bar.dart b/lib/src/features/post/presentation/widgets/post_page_app_bar.dart index d9575102a..c4298aae7 100644 --- a/lib/src/features/post/presentation/widgets/post_page_app_bar.dart +++ b/lib/src/features/post/presentation/widgets/post_page_app_bar.dart @@ -57,7 +57,7 @@ class PostPageAppBar extends StatelessWidget { @override Widget build(BuildContext context) { - final state = context.read().state; + final state = context.read().state; return SliverAppBar( pinned: !state.hideTopBarOnScroll, diff --git a/lib/src/features/post/presentation/widgets/post_page_fab.dart b/lib/src/features/post/presentation/widgets/post_page_fab.dart index 5c81926bf..b5cd678e6 100644 --- a/lib/src/features/post/presentation/widgets/post_page_fab.dart +++ b/lib/src/features/post/presentation/widgets/post_page_fab.dart @@ -4,14 +4,15 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; +import 'package:thunder/src/app/shell/state/shell_chrome_cubit.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/src/features/post/post.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/shared/sort_picker.dart'; import 'package:thunder/src/shared/gesture_fab.dart'; @@ -47,7 +48,7 @@ class _PostPageFABState extends State { void showSortBottomSheet(BuildContext context) { final l10n = GlobalContext.l10n; - final account = context.read().state.account; + final account = resolveEffectiveAccount(context); final commentSortType = context.read().state.commentSortType; HapticFeedback.mediumImpact(); @@ -71,7 +72,7 @@ class _PostPageFABState extends State { void replyToPost(BuildContext context, ThunderPost? post, {bool postLocked = false}) async { final l10n = GlobalContext.l10n; - final isLoggedIn = context.read().state.isLoggedIn; + final isLoggedIn = !resolveEffectiveAccount(context).anonymous; if (postLocked) return showSnackbar(l10n.postLocked); if (!isLoggedIn) return showSnackbar(l10n.mustBeLoggedInComment); @@ -142,10 +143,10 @@ class _PostPageFABState extends State { final l10n = GlobalContext.l10n; final combineNavAndFab = context.select((cubit) => cubit.state.combineNavAndFab); - final isFabSummoned = context.select((cubit) => cubit.state.isPostFabSummoned); + final isFabSummoned = context.select((cubit) => cubit.state.isPostFabSummoned); final singlePressAction = context.select((cubit) => cubit.state.postFabSinglePressAction); final longPressAction = context.select((cubit) => cubit.state.postFabLongPressAction); - final hideTopBarOnScroll = context.select((bloc) => bloc.state.hideTopBarOnScroll); + final hideTopBarOnScroll = context.select((bloc) => bloc.state.hideTopBarOnScroll); final enableCommentNavigation = context.select((cubit) => cubit.state.enableCommentNavigation); final enablePostsFab = context.select((cubit) => cubit.state.enablePostsFab); final postFabEnableRefresh = context.select((cubit) => cubit.state.postFabEnableRefresh); diff --git a/lib/src/features/search/presentation/pages/search_page.dart b/lib/src/features/search/presentation/pages/search_page.dart index 691da04af..35134c60c 100644 --- a/lib/src/features/search/presentation/pages/search_page.dart +++ b/lib/src/features/search/presentation/pages/search_page.dart @@ -21,10 +21,12 @@ import 'package:thunder/packages/ui/ui.dart' show ListPickerItem; /// The main search page that handles search functionality. class SearchPage extends StatefulWidget { + final Account account; + /// Limits the search to a specific community. final ThunderCommunity? community; - const SearchPage({super.key, this.community}); + const SearchPage({super.key, required this.account, this.community}); @override State createState() => _SearchPageState(); @@ -98,7 +100,7 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi void onScroll() { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent * 0.8) { final bloc = context.read(); - final favorites = context.read().state.favorites; + final favorites = _favoriteCommunities(); final query = controller.text; if (query.isEmpty && !bloc.state.viewingAll) return; @@ -142,7 +144,7 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi } if (controller.text.isNotEmpty || force || bloc.state.viewingAll) { - final favorites = context.read().state.favorites; + final favorites = _favoriteCommunities(); final triggerSearch = force || bloc.state.viewingAll; bloc.add(SearchStarted(query: controller.text, force: triggerSearch, favoriteCommunities: favorites)); @@ -151,6 +153,14 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi } } + List _favoriteCommunities() { + try { + return context.read().state.favorites; + } catch (_) { + return const []; + } + } + void showSortPicker() { final l10n = GlobalContext.l10n; final feedBloc = context.read(); @@ -211,7 +221,8 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi super.build(context); final l10n = GlobalContext.l10n; - final account = context.select((bloc) => bloc.state.account); + final account = widget.account; + final favorites = _favoriteCommunities(); return BlocListener( listenWhen: (previous, current) => previous.focusSearchId != current.focusSearchId, @@ -239,6 +250,8 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi onShowSortPicker: showSortPicker, ), SearchBody( + account: account, + favorites: favorites, scrollController: scrollController, communityToSearch: widget.community, accountInstance: account.instance, diff --git a/lib/src/features/search/presentation/widgets/search_body.dart b/lib/src/features/search/presentation/widgets/search_body.dart index 9b1be7b3b..7dd5688d7 100644 --- a/lib/src/features/search/presentation/widgets/search_body.dart +++ b/lib/src/features/search/presentation/widgets/search_body.dart @@ -19,6 +19,9 @@ import 'package:thunder/packages/ui/ui.dart' show ThunderActionChip; /// The main body content of the search page showing results based on search state. class SearchBody extends StatelessWidget { + final Account account; + final List favorites; + /// The scroll controller for infinite scrolling. final ScrollController scrollController; @@ -42,6 +45,8 @@ class SearchBody extends StatelessWidget { const SearchBody({ super.key, + required this.account, + required this.favorites, required this.scrollController, required this.communityToSearch, required this.accountInstance, @@ -69,6 +74,7 @@ class SearchBody extends StatelessWidget { case SearchStatus.initial: case SearchStatus.trending: return _SearchInitialView( + favorites: favorites, communityToSearch: communityToSearch, accountInstance: accountInstance, isQueryEmpty: isQueryEmpty, @@ -83,6 +89,7 @@ class SearchBody extends StatelessWidget { case SearchStatus.done: case SearchStatus.performingCommentAction: return _SearchResultsView( + account: account, scrollController: scrollController, communityToSearch: communityToSearch, onSetSearchType: onSetSearchType, @@ -101,6 +108,7 @@ class SearchBody extends StatelessWidget { /// Widget that displays the initial view when no search has been performed. class _SearchInitialView extends StatelessWidget { + final List favorites; final ThunderCommunity? communityToSearch; final String accountInstance; final bool isQueryEmpty; @@ -109,6 +117,7 @@ class _SearchInitialView extends StatelessWidget { final MetaSearchType searchType; const _SearchInitialView({ + required this.favorites, required this.communityToSearch, required this.accountInstance, required this.isQueryEmpty, @@ -133,6 +142,7 @@ class _SearchInitialView extends StatelessWidget { ), secondChild: showTrending ? _SearchTrendingView( + favorites: favorites, trendingCommunities: trendingCommunities!, accountInstance: accountInstance, onViewAll: onViewAll, @@ -200,11 +210,13 @@ class _SearchEmptyPrompt extends StatelessWidget { /// Widget that displays the trending communities view. class _SearchTrendingView extends StatelessWidget { + final List favorites; final List trendingCommunities; final String accountInstance; final VoidCallback onViewAll; const _SearchTrendingView({ + required this.favorites, required this.trendingCommunities, required this.accountInstance, required this.onViewAll, @@ -215,66 +227,63 @@ class _SearchTrendingView extends StatelessWidget { final theme = Theme.of(context); final l10n = GlobalContext.l10n; - return BlocSelector>( - selector: (state) => state.favorites, - builder: (context, favorites) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (favorites.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), + child: Text(l10n.favorites, style: theme.textTheme.titleLarge), + ), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: favorites.length, + itemBuilder: (context, index) => CommunityListEntry(community: favorites[index], indicateFavorites: false), + ), + const SizedBox(height: 20), + ], + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), + child: Text(l10n.trendingCommunities, style: theme.textTheme.titleLarge), + ), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: trendingCommunities.length, + itemBuilder: (context, index) => CommunityListEntry(community: trendingCommunities[index]), + ), + const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - if (favorites.isNotEmpty) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), - child: Text(l10n.favorites, style: theme.textTheme.titleLarge), - ), - ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: favorites.length, - itemBuilder: (context, index) => CommunityListEntry(community: favorites[index], indicateFavorites: false), - ), - const SizedBox(height: 20), - ], - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), - child: Text(l10n.trendingCommunities, style: theme.textTheme.titleLarge), - ), - ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: trendingCommunities.length, - itemBuilder: (context, index) => CommunityListEntry(community: trendingCommunities[index]), + ThunderActionChip(label: l10n.viewAll, onPressed: onViewAll), + const SizedBox(width: 10), + ThunderActionChip( + trailingIcon: Icons.chevron_right_rounded, + label: l10n.exploreInstance, + onPressed: () => navigateToInstancePage(context, instanceHost: accountInstance, instanceId: null), ), - const SizedBox(height: 5), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ThunderActionChip(label: l10n.viewAll, onPressed: onViewAll), - const SizedBox(width: 10), - ThunderActionChip( - trailingIcon: Icons.chevron_right_rounded, - label: l10n.exploreInstance, - onPressed: () => navigateToInstancePage(context, instanceHost: accountInstance, instanceId: null), - ), - ], - ), - const SizedBox(height: 10), ], ), - ); - }, + const SizedBox(height: 10), + ], + ), ); } } /// Widget that displays search results based on the current search type. class _SearchResultsView extends StatelessWidget { + final Account account; final ScrollController scrollController; final ThunderCommunity? communityToSearch; final ValueChanged onSetSearchType; final SearchState state; const _SearchResultsView({ + required this.account, required this.scrollController, required this.communityToSearch, required this.onSetSearchType, @@ -291,19 +300,14 @@ class _SearchResultsView extends StatelessWidget { ); } - return BlocSelector( - selector: (state) => state.account, - builder: (context, account) { - return switch (state.searchType) { - MetaSearchType.communities => SearchCommunitiesResults(scrollController: scrollController), - MetaSearchType.users => SearchUsersResults(scrollController: scrollController), - MetaSearchType.comments => SearchCommentsResults(scrollController: scrollController), - MetaSearchType.posts => SearchPostsResults(scrollController: scrollController, account: account), - MetaSearchType.instances => SearchInstancesResults(scrollController: scrollController), - _ => const SizedBox.shrink(), - }; - }, - ); + return switch (state.searchType) { + MetaSearchType.communities => SearchCommunitiesResults(scrollController: scrollController), + MetaSearchType.users => SearchUsersResults(scrollController: scrollController), + MetaSearchType.comments => SearchCommentsResults(scrollController: scrollController), + MetaSearchType.posts => SearchPostsResults(scrollController: scrollController, account: account), + MetaSearchType.instances => SearchInstancesResults(scrollController: scrollController), + _ => const SizedBox.shrink(), + }; } } diff --git a/lib/src/features/search/presentation/widgets/search_posts_results.dart b/lib/src/features/search/presentation/widgets/search_posts_results.dart index bb9b59369..33db7f593 100644 --- a/lib/src/features/search/presentation/widgets/search_posts_results.dart +++ b/lib/src/features/search/presentation/widgets/search_posts_results.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; -import 'package:thunder/src/app/wiring/state_factories.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/features/post/post.dart'; @@ -24,67 +23,73 @@ class SearchPostsResults extends StatefulWidget { } class _SearchPostsResultsState extends State { - late final FeedBloc _feedBloc; + late final PostListActionController _postListActionController; + List _posts = const []; @override void initState() { super.initState(); - _feedBloc = createFeedBloc(widget.account); + _postListActionController = PostListActionController(postRepository: PostRepositoryImpl(account: widget.account)); + _posts = context.read().state.posts ?? const []; + } - // Initialize with current posts - final posts = context.read().state.posts; - if (posts != null && posts.isNotEmpty) { - _feedBloc.add(PopulatePostsEvent(posts)); - } + void _setPosts(List posts) { + if (!mounted) return; + setState(() => _posts = posts); } - @override - void dispose() { - _feedBloc.close(); - super.dispose(); + Future _handleVoteAction(ThunderPost post, int voteType) async => _setPosts(await _postListActionController.vote(_posts, post, voteType)); + + Future _handleSaveAction(ThunderPost post, bool saved) async => _setPosts(await _postListActionController.save(_posts, post, saved)); + + Future _handleReadAction(ThunderPost post, bool read) async => _setPosts(await _postListActionController.read(_posts, post, read)); + + Future _handleHideAction(ThunderPost post, bool hidden) async => _setPosts(await _postListActionController.hide(_posts, post, hidden)); + + Future _handleMultiReadAction(List postIds, bool read) async => _setPosts(await _postListActionController.multiRead(_posts, postIds, read)); + + void _handleSourcePostsChanged(List sourcePosts) { + _setPosts(_postListActionController.reconcile(sourcePosts: sourcePosts, currentPosts: _posts)); } @override Widget build(BuildContext context) { - final tabletMode = context.select((bloc) => bloc.state.tabletMode); - - return BlocProvider.value( - value: _feedBloc, - child: BlocListener( - listenWhen: (previous, current) => previous.posts != current.posts, - listener: (context, state) { - _feedBloc.add(PopulatePostsEvent(state.posts ?? [])); - }, - child: BlocSelector( - selector: (state) => state.status, - builder: (context, status) { - // Read posts from FeedBloc - this ensures post actions are reflected in the UI - return BlocSelector>( - selector: (state) => state.posts, - builder: (context, posts) { - return CustomScrollView( - controller: widget.scrollController, - slivers: [ - FeedPostCardList( - posts: posts, - tabletMode: tabletMode, - markPostReadOnScroll: false, + final tabletMode = context.select((bloc) => bloc.state.tabletMode); + + return BlocListener( + listenWhen: (previous, current) => previous.posts != current.posts, + listener: (context, state) => _handleSourcePostsChanged(state.posts ?? const []), + child: BlocSelector( + selector: (state) => state.status, + builder: (context, status) { + return CustomScrollView( + controller: widget.scrollController, + slivers: [ + FeedPostCardList( + posts: _posts, + tabletMode: tabletMode, + markPostReadOnScroll: false, + onVoteAction: _handleVoteAction, + onSaveAction: _handleSaveAction, + onReadAction: _handleReadAction, + onHideAction: _handleHideAction, + onMultiReadAction: _handleMultiReadAction, + onPostUpdated: (post) => _setPosts(_postListActionController.updatePost(_posts, post)), + onDismissHiddenPost: (postId) => _setPosts(_postListActionController.dismissHiddenPost(_posts, postId)), + onDismissBlocked: ({userId, communityId}) => _setPosts(_postListActionController.dismissBlocked(_posts, userId: userId, communityId: communityId)), + ), + if (status == SearchStatus.refreshing) + const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.only(bottom: 10.0), + child: CircularProgressIndicator(), ), - if (status == SearchStatus.refreshing) - const SliverToBoxAdapter( - child: Center( - child: Padding( - padding: EdgeInsets.only(bottom: 10.0), - child: CircularProgressIndicator(), - ), - ), - ), - ], - ); - }, - ); - }, - ), + ), + ), + ], + ); + }, ), ); } diff --git a/lib/src/features/session/api.dart b/lib/src/features/session/api.dart new file mode 100644 index 000000000..77be036ba --- /dev/null +++ b/lib/src/features/session/api.dart @@ -0,0 +1 @@ +export 'session.dart'; diff --git a/lib/src/features/session/data/repositories/persistent_session_repository.dart b/lib/src/features/session/data/repositories/persistent_session_repository.dart new file mode 100644 index 000000000..6a053dd2a --- /dev/null +++ b/lib/src/features/session/data/repositories/persistent_session_repository.dart @@ -0,0 +1,137 @@ +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; + +import 'package:thunder/src/foundation/config/config.dart'; +import 'package:thunder/src/foundation/contracts/account.dart'; +import 'package:thunder/src/foundation/persistence/persistence.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; +import 'package:thunder/src/features/session/domain/repositories/session_repository.dart'; + +class PersistentSessionRepository implements SessionRepository { + const PersistentSessionRepository(); + + @override + Future bootstrap() async { + final driftAccount = await _resolveDriftActiveSession(); + if (driftAccount != null) { + return driftAccount; + } + + final existingSessions = await getSessions(); + if (existingSessions.isNotEmpty) { + final fallbackSession = existingSessions.first; + await _persistActiveSession(fallbackSession); + return fallbackSession; + } + + final account = await addAnonymousSession(const Account(id: '', instance: DEFAULT_INSTANCE, index: -1, anonymous: true, platform: ThreadiversePlatform.lemmy)); + if (account == null) throw Exception('Failed to create default profile'); + + await _persistActiveSession(account); + + return account; + } + + Future _resolveDriftActiveSession() async { + final storedSession = await (database.select(database.sessionStateTable)..where((table) => table.singleton.equals(0))).getSingleOrNull(); + if (storedSession?.accountId == null) return null; + + final activeAccount = await Account.fetchAccount(storedSession!.accountId!.toString()); + if (activeAccount != null) return activeAccount; + + await (database.delete(database.sessionStateTable)..where((table) => table.singleton.equals(0))).go(); + return null; + } + + @override + Future> getAuthenticatedSessions() { + return Account.accounts(); + } + + @override + Future> getAnonymousSessions() { + return Account.anonymousInstances(); + } + + @override + Future> getSessions() async { + final authenticatedSessions = await getAuthenticatedSessions(); + final anonymousSessions = await getAnonymousSessions(); + final sessions = [...authenticatedSessions, ...anonymousSessions]; + sessions.sort((a, b) => a.index.compareTo(b.index)); + return sessions; + } + + @override + Future getSessionByKey(String sessionKey) async { + final account = int.tryParse(sessionKey) != null ? await Account.fetchAccount(sessionKey) : null; + if (account != null) return account; + + final anonymousSessions = await getAnonymousSessions(); + return anonymousSessions.firstWhereOrNull((session) => session.instance == sessionKey); + } + + @override + Future setActiveSession(Account account) async { + await _persistActiveSession(account); + } + + Future _persistActiveSession(Account account) async { + await database.into(database.sessionStateTable).insertOnConflictUpdate(SessionStateTableCompanion.insert(singleton: const Value(0), accountId: Value(int.parse(account.id)))); + } + + @override + Future clearActiveSession() async { + await (database.delete(database.sessionStateTable)..where((table) => table.singleton.equals(0))).go(); + } + + @override + Future addAuthenticatedSession(Account account) { + return Account.insertAccount(account); + } + + @override + Future addAnonymousSession(Account account) { + return Account.insertAnonymousInstance(account); + } + + @override + Future removeSession(String sessionKey) async { + final account = int.tryParse(sessionKey) != null ? await Account.fetchAccount(sessionKey) : null; + final activeSession = await _resolveDriftActiveSession(); + final isRemovingActiveAuthenticatedSession = account != null && activeSession?.id == account.id; + final isRemovingActiveAnonymousSession = account == null && activeSession?.instance == sessionKey; + + if (account != null) { + await Account.deleteAccount(sessionKey); + if (isRemovingActiveAuthenticatedSession) { + await _promoteFallbackSession(); + } + return; + } + + await Account.deleteAnonymousInstance(sessionKey); + if (isRemovingActiveAnonymousSession) { + await _promoteFallbackSession(); + } + } + + Future _promoteFallbackSession() async { + final anonymousSessions = await getAnonymousSessions(); + if (anonymousSessions.isNotEmpty) { + await _persistActiveSession(anonymousSessions.sorted((a, b) => a.index.compareTo(b.index)).last); + return; + } + + final authenticatedSessions = await getAuthenticatedSessions(); + if (authenticatedSessions.isNotEmpty) { + await _persistActiveSession(authenticatedSessions.sorted((a, b) => a.index.compareTo(b.index)).last); + return; + } + + await clearActiveSession(); + final account = await addAnonymousSession(const Account(id: '', instance: DEFAULT_INSTANCE, index: -1, anonymous: true, platform: ThreadiversePlatform.lemmy)); + if (account == null) throw Exception('Failed to create default profile'); + await _persistActiveSession(account); + } +} diff --git a/lib/src/features/session/domain/models/feature_account.dart b/lib/src/features/session/domain/models/feature_account.dart new file mode 100644 index 000000000..ca3fd5112 --- /dev/null +++ b/lib/src/features/session/domain/models/feature_account.dart @@ -0,0 +1,30 @@ +import 'package:equatable/equatable.dart'; + +import 'package:thunder/src/features/account/account.dart'; + +enum FeatureAccountOverrideStatus { inactive, active } + +class FeatureAccount extends Equatable { + const FeatureAccount({required this.baseAccount, this.overrideAccount}); + + final Account baseAccount; + final Account? overrideAccount; + + Account get effectiveAccount => overrideAccount ?? baseAccount; + + bool get hasOverride => overrideAccount != null && overrideAccount!.id != baseAccount.id; + + FeatureAccountOverrideStatus get overrideStatus => hasOverride ? FeatureAccountOverrideStatus.active : FeatureAccountOverrideStatus.inactive; + + FeatureAccount copyWith({Account? baseAccount, Object? overrideAccount = _sentinel}) { + return FeatureAccount( + baseAccount: baseAccount ?? this.baseAccount, + overrideAccount: identical(overrideAccount, _sentinel) ? this.overrideAccount : overrideAccount as Account?, + ); + } + + @override + List get props => [baseAccount, overrideAccount, effectiveAccount, hasOverride, overrideStatus]; +} + +const Object _sentinel = Object(); diff --git a/lib/src/features/session/domain/models/feature_account_resolution_request.dart b/lib/src/features/session/domain/models/feature_account_resolution_request.dart new file mode 100644 index 000000000..50467c3d3 --- /dev/null +++ b/lib/src/features/session/domain/models/feature_account_resolution_request.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; + +class FeatureAccountResolutionRequest extends Equatable { + const FeatureAccountResolutionRequest({ + this.communityActorId, + this.postActorId, + this.parentCommentActorId, + }); + + final String? communityActorId; + final String? postActorId; + final String? parentCommentActorId; + + bool get hasTargets => _hasValue(communityActorId) || _hasValue(postActorId) || _hasValue(parentCommentActorId); + + @override + List get props => [communityActorId, postActorId, parentCommentActorId]; +} + +bool _hasValue(String? value) => value?.isNotEmpty == true; diff --git a/lib/src/features/session/domain/models/feature_account_resolved_content.dart b/lib/src/features/session/domain/models/feature_account_resolved_content.dart new file mode 100644 index 000000000..8e8feea22 --- /dev/null +++ b/lib/src/features/session/domain/models/feature_account_resolved_content.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; + +import 'package:thunder/src/features/comment/comment.dart'; +import 'package:thunder/src/features/community/community.dart'; +import 'package:thunder/src/features/post/post.dart'; + +class FeatureAccountResolvedContent extends Equatable { + const FeatureAccountResolvedContent({ + this.community, + this.post, + this.parentComment, + }); + + final ThunderCommunity? community; + final ThunderPost? post; + final ThunderComment? parentComment; + + @override + List get props => [community, post, parentComment]; +} diff --git a/lib/src/features/session/domain/repositories/session_repository.dart b/lib/src/features/session/domain/repositories/session_repository.dart new file mode 100644 index 000000000..a12e041ab --- /dev/null +++ b/lib/src/features/session/domain/repositories/session_repository.dart @@ -0,0 +1,23 @@ +import 'package:thunder/src/foundation/contracts/account.dart'; + +abstract class SessionRepository { + Future bootstrap(); + + Future> getAuthenticatedSessions(); + + Future> getAnonymousSessions(); + + Future> getSessions(); + + Future getSessionByKey(String sessionKey); + + Future setActiveSession(Account account); + + Future clearActiveSession(); + + Future addAuthenticatedSession(Account account); + + Future addAnonymousSession(Account account); + + Future removeSession(String sessionKey); +} diff --git a/lib/src/features/session/domain/services/feature_account_content_resolver.dart b/lib/src/features/session/domain/services/feature_account_content_resolver.dart new file mode 100644 index 000000000..1e4ae0fe3 --- /dev/null +++ b/lib/src/features/session/domain/services/feature_account_content_resolver.dart @@ -0,0 +1,74 @@ +import 'package:flutter/foundation.dart'; + +import 'package:thunder/src/features/account/account.dart'; +import 'package:thunder/src/features/comment/comment.dart'; +import 'package:thunder/src/features/community/community.dart'; +import 'package:thunder/src/features/post/post.dart'; +import 'package:thunder/src/features/search/search.dart'; +import 'package:thunder/src/features/session/domain/models/feature_account_resolution_request.dart'; +import 'package:thunder/src/features/session/domain/models/feature_account_resolved_content.dart'; + +class FeatureAccountContentResolver { + FeatureAccountContentResolver({ + SearchRepository Function(Account account)? searchRepositoryFactory, + Future> Function(List posts)? postParser, + }) : _searchRepositoryFactory = searchRepositoryFactory ?? ((account) => SearchRepositoryImpl(account: account)), + _postParser = postParser ?? ((posts) => parsePosts(posts)); + + final SearchRepository Function(Account account) _searchRepositoryFactory; + final Future> Function(List posts) _postParser; + + Future resolve({required Account account, required FeatureAccountResolutionRequest request}) async { + final repository = _searchRepositoryFactory(account); + + final community = await _resolveCommunity(repository, request.communityActorId); + final post = await _resolvePost(repository, request.postActorId); + final parentComment = await _resolveParentComment(repository, request.parentCommentActorId); + + return FeatureAccountResolvedContent( + community: community, + post: post, + parentComment: parentComment, + ); + } + + Future _resolveCommunity(SearchRepository repository, String? actorId) async { + if (actorId?.isNotEmpty != true) return null; + + try { + final response = await repository.resolve(query: actorId!); + return response.community; + } catch (error) { + debugPrint('Failed to resolve community: $error'); + return null; + } + } + + Future _resolvePost(SearchRepository repository, String? actorId) async { + if (actorId?.isNotEmpty != true) return null; + + try { + final response = await repository.resolve(query: actorId!); + final post = response.post; + if (post == null) return null; + + final parsedPosts = await _postParser([post]); + return parsedPosts.isNotEmpty ? parsedPosts.first : null; + } catch (error) { + debugPrint('Failed to resolve post: $error'); + return null; + } + } + + Future _resolveParentComment(SearchRepository repository, String? actorId) async { + if (actorId?.isNotEmpty != true) return null; + + try { + final response = await repository.resolve(query: actorId!); + return response.comment; + } catch (error) { + debugPrint('Failed to resolve parent comment: $error'); + return null; + } + } +} diff --git a/lib/src/features/session/presentation/state/feature_account_cubit.dart b/lib/src/features/session/presentation/state/feature_account_cubit.dart new file mode 100644 index 000000000..c2912bbec --- /dev/null +++ b/lib/src/features/session/presentation/state/feature_account_cubit.dart @@ -0,0 +1,35 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +import 'package:thunder/src/features/account/account.dart'; +import 'package:thunder/src/features/session/domain/models/feature_account.dart'; + +part 'feature_account_state.dart'; + +class FeatureAccountCubit extends Cubit { + FeatureAccountCubit({required Account baseAccount}) : super(FeatureAccountState.initial(baseAccount)); + + void syncBaseAccount(Account baseAccount) { + final overrideAccount = state.overrideAccount; + + if (overrideAccount != null && overrideAccount.id == baseAccount.id) { + emit(FeatureAccountState.initial(baseAccount)); + return; + } + + emit(state.copyWith(featureAccount: state.featureAccount.copyWith(baseAccount: baseAccount))); + } + + void setOverride(Account overrideAccount) { + if (overrideAccount.id == state.baseAccount.id) { + clearOverride(); + return; + } + + emit(state.copyWith(featureAccount: state.featureAccount.copyWith(overrideAccount: overrideAccount))); + } + + void clearOverride() { + emit(state.copyWith(featureAccount: state.featureAccount.copyWith(overrideAccount: null))); + } +} diff --git a/lib/src/features/session/presentation/state/feature_account_state.dart b/lib/src/features/session/presentation/state/feature_account_state.dart new file mode 100644 index 000000000..7179cd151 --- /dev/null +++ b/lib/src/features/session/presentation/state/feature_account_state.dart @@ -0,0 +1,28 @@ +part of 'feature_account_cubit.dart'; + +class FeatureAccountState extends Equatable { + const FeatureAccountState({required this.featureAccount}); + + factory FeatureAccountState.initial(Account baseAccount) { + return FeatureAccountState(featureAccount: FeatureAccount(baseAccount: baseAccount)); + } + + final FeatureAccount featureAccount; + + Account get baseAccount => featureAccount.baseAccount; + + Account get effectiveAccount => featureAccount.effectiveAccount; + + Account? get overrideAccount => featureAccount.overrideAccount; + + bool get hasOverride => featureAccount.hasOverride; + + FeatureAccountOverrideStatus get overrideStatus => featureAccount.overrideStatus; + + FeatureAccountState copyWith({FeatureAccount? featureAccount}) { + return FeatureAccountState(featureAccount: featureAccount ?? this.featureAccount); + } + + @override + List get props => [featureAccount, baseAccount, effectiveAccount, overrideAccount, hasOverride, overrideStatus]; +} diff --git a/lib/src/features/session/presentation/state/session_bloc.dart b/lib/src/features/session/presentation/state/session_bloc.dart new file mode 100644 index 000000000..391b1defe --- /dev/null +++ b/lib/src/features/session/presentation/state/session_bloc.dart @@ -0,0 +1,173 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; + +import 'package:thunder/src/foundation/contracts/contracts.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; +import 'package:thunder/src/features/account/account.dart'; +import 'package:thunder/src/features/instance/instance.dart'; +import 'package:thunder/src/features/session/domain/repositories/session_repository.dart'; + +part 'session_event.dart'; +part 'session_state.dart'; + +class SessionBloc extends Bloc { + SessionBloc({ + required SessionRepository sessionRepository, + required AccountRepository Function(Account) accountRepositoryFactory, + required InstanceRepository Function(Account) instanceRepositoryFactory, + required PlatformDetectionService platformDetectionService, + required LocalizationService localizationService, + }) : _sessionRepository = sessionRepository, + _accountRepositoryFactory = accountRepositoryFactory, + _instanceRepositoryFactory = instanceRepositoryFactory, + _platformDetectionService = platformDetectionService, + _localizationService = localizationService, + super(const SessionState()) { + on(_onBootstrapRequested); + on(_onSwitched); + on(_onRemoved); + on(_onAuthenticatedSessionAdded); + on(_onAnonymousSessionAdded); + on(_onAuthenticatedLoginRequested); + } + + final SessionRepository _sessionRepository; + final AccountRepository Function(Account) _accountRepositoryFactory; + final InstanceRepository Function(Account) _instanceRepositoryFactory; + final PlatformDetectionService _platformDetectionService; + final LocalizationService _localizationService; + + Future _onBootstrapRequested(SessionInitialized event, Emitter emit) async { + emit(state.copyWith(status: SessionStatus.loading, mutationStatus: SessionMutationStatus.idle, lastMutation: () => null, error: () => null)); + await _refreshState(emit, status: SessionStatus.success); + } + + Future _onSwitched(SessionSwitched event, Emitter emit) async { + emit(state.copyWith(mutationStatus: SessionMutationStatus.loading, lastMutation: () => SessionMutationType.switchSession, error: () => null)); + + try { + final session = await _sessionRepository.getSessionByKey(event.sessionKey); + if (session == null) { + emit(state.copyWith(mutationStatus: SessionMutationStatus.failure, error: () => 'Unable to resolve session ${event.sessionKey}')); + return; + } + + await _sessionRepository.setActiveSession(session); + await _refreshState(emit, mutationStatus: SessionMutationStatus.success, mutationType: SessionMutationType.switchSession); + } catch (error) { + emit(state.copyWith(mutationStatus: SessionMutationStatus.failure, error: () => error.toString())); + } + } + + Future _onRemoved(SessionRemoved event, Emitter emit) async { + emit(state.copyWith(mutationStatus: SessionMutationStatus.loading, lastMutation: () => SessionMutationType.removeSession, error: () => null)); + + try { + await _sessionRepository.removeSession(event.sessionKey); + await _refreshState(emit, mutationStatus: SessionMutationStatus.success, mutationType: SessionMutationType.removeSession); + } catch (error) { + emit(state.copyWith(mutationStatus: SessionMutationStatus.failure, error: () => error.toString())); + } + } + + Future _onAuthenticatedSessionAdded(AuthenticatedSessionAdded event, Emitter emit) async { + await _persistSession(emit, account: event.account, activate: event.activate, isAnonymous: false); + } + + Future _onAnonymousSessionAdded(AnonymousSessionAdded event, Emitter emit) async { + await _persistSession(emit, account: event.account, activate: event.activate, isAnonymous: true); + } + + Future _onAuthenticatedLoginRequested(AuthenticatedLoginRequested event, Emitter emit) async { + emit(state.copyWith(mutationStatus: SessionMutationStatus.loading, lastMutation: () => SessionMutationType.authenticatedLogin, error: () => null)); + + try { + final instanceUrl = event.instance.replaceAll('https://', '').trim(); + final platformInfo = await _platformDetectionService.detectPlatform(instanceUrl) ?? {'platform': ThreadiversePlatform.lemmy}; + final platform = platformInfo['platform'] as ThreadiversePlatform; + + var tempAccount = Account(id: '', index: -1, instance: instanceUrl, platform: platform); + final jwt = await _accountRepositoryFactory(tempAccount).login(username: event.username, password: event.password, totp: event.totp); + + if (jwt == null) { + final message = _localizationService.l10n.unexpectedError; + emit(state.copyWith(mutationStatus: SessionMutationStatus.failure, error: () => message)); + return; + } + + tempAccount = Account(id: '', index: -1, jwt: jwt, instance: tempAccount.instance, platform: platform); + final siteResponse = await _instanceRepositoryFactory(tempAccount).info(); + + final persistedSession = await _sessionRepository.addAuthenticatedSession(Account( + id: '', + username: siteResponse.myUser?.localUserView.person.name, + jwt: jwt, + instance: tempAccount.instance, + userId: siteResponse.myUser?.localUserView.person.id, + index: -1, + platform: platform, + )); + + if (persistedSession == null) { + final message = _localizationService.l10n.unexpectedError; + emit(state.copyWith(mutationStatus: SessionMutationStatus.failure, error: () => message)); + return; + } + + await _sessionRepository.setActiveSession(persistedSession); + await _refreshState(emit, mutationStatus: SessionMutationStatus.success, mutationType: SessionMutationType.authenticatedLogin); + } catch (error) { + emit(state.copyWith(mutationStatus: SessionMutationStatus.failure, error: () => error.toString())); + } + } + + Future _persistSession( + Emitter emit, { + required Account account, + required bool activate, + required bool isAnonymous, + }) async { + final mutationType = isAnonymous ? SessionMutationType.addAnonymousSession : SessionMutationType.addAuthenticatedSession; + emit(state.copyWith(mutationStatus: SessionMutationStatus.loading, lastMutation: () => mutationType, error: () => null)); + + try { + final persistedSession = isAnonymous ? await _sessionRepository.addAnonymousSession(account) : await _sessionRepository.addAuthenticatedSession(account); + if (persistedSession == null) { + emit(state.copyWith(mutationStatus: SessionMutationStatus.failure, error: () => 'Unable to persist session')); + return; + } + + if (activate) await _sessionRepository.setActiveSession(persistedSession); + + await _refreshState(emit, mutationStatus: SessionMutationStatus.success, mutationType: mutationType); + } catch (error) { + emit(state.copyWith(mutationStatus: SessionMutationStatus.failure, error: () => error.toString())); + } + } + + Future _refreshState( + Emitter emit, { + SessionStatus status = SessionStatus.success, + SessionMutationStatus? mutationStatus, + SessionMutationType? mutationType, + }) async { + try { + final activeAccount = await _sessionRepository.bootstrap(); + final authenticatedSessions = await _sessionRepository.getAuthenticatedSessions(); + final anonymousSessions = await _sessionRepository.getAnonymousSessions(); + + emit(state.copyWith( + status: status, + mutationStatus: mutationStatus ?? state.mutationStatus, + lastMutation: () => mutationType ?? state.lastMutation, + generation: state.generation + 1, + activeAccount: () => activeAccount, + authenticatedSessions: authenticatedSessions, + anonymousSessions: anonymousSessions, + error: () => null)); + } catch (error) { + emit(state.copyWith(status: SessionStatus.failure, error: () => error.toString())); + } + } +} diff --git a/lib/src/features/session/presentation/state/session_event.dart b/lib/src/features/session/presentation/state/session_event.dart new file mode 100644 index 000000000..c7ed80e0f --- /dev/null +++ b/lib/src/features/session/presentation/state/session_event.dart @@ -0,0 +1,62 @@ +part of 'session_bloc.dart'; + +sealed class SessionEvent extends Equatable { + const SessionEvent(); + + @override + List get props => []; +} + +class SessionInitialized extends SessionEvent { + const SessionInitialized(); +} + +class SessionSwitched extends SessionEvent { + const SessionSwitched({required this.sessionKey}); + + final String sessionKey; + + @override + List get props => [sessionKey]; +} + +class SessionRemoved extends SessionEvent { + const SessionRemoved({required this.sessionKey}); + + final String sessionKey; + + @override + List get props => [sessionKey]; +} + +class AuthenticatedSessionAdded extends SessionEvent { + const AuthenticatedSessionAdded({required this.account, this.activate = true}); + + final Account account; + final bool activate; + + @override + List get props => [account, activate]; +} + +class AnonymousSessionAdded extends SessionEvent { + const AnonymousSessionAdded({required this.account, this.activate = false}); + + final Account account; + final bool activate; + + @override + List get props => [account, activate]; +} + +class AuthenticatedLoginRequested extends SessionEvent { + const AuthenticatedLoginRequested({required this.username, required this.password, required this.instance, this.totp = ''}); + + final String username; + final String password; + final String instance; + final String totp; + + @override + List get props => [username, password, instance, totp]; +} diff --git a/lib/src/features/session/presentation/state/session_state.dart b/lib/src/features/session/presentation/state/session_state.dart new file mode 100644 index 000000000..6ceb8f394 --- /dev/null +++ b/lib/src/features/session/presentation/state/session_state.dart @@ -0,0 +1,56 @@ +part of 'session_bloc.dart'; + +enum SessionStatus { initial, loading, success, failure } + +enum SessionMutationStatus { idle, loading, success, failure } + +enum SessionMutationType { switchSession, removeSession, addAuthenticatedSession, addAnonymousSession, authenticatedLogin } + +class SessionState extends Equatable { + const SessionState({ + this.status = SessionStatus.initial, + this.mutationStatus = SessionMutationStatus.idle, + this.lastMutation, + this.generation = 0, + this.activeAccount, + this.authenticatedSessions = const [], + this.anonymousSessions = const [], + this.error, + }); + + final SessionStatus status; + final SessionMutationStatus mutationStatus; + final SessionMutationType? lastMutation; + final int generation; + final Account? activeAccount; + final List authenticatedSessions; + final List anonymousSessions; + final String? error; + + List get sessions => [...authenticatedSessions, ...anonymousSessions]..sort((a, b) => a.index.compareTo(b.index)); + + SessionState copyWith({ + SessionStatus? status, + SessionMutationStatus? mutationStatus, + ValueGetter? lastMutation, + int? generation, + ValueGetter? activeAccount, + List? authenticatedSessions, + List? anonymousSessions, + ValueGetter? error, + }) { + return SessionState( + status: status ?? this.status, + mutationStatus: mutationStatus ?? this.mutationStatus, + lastMutation: lastMutation != null ? lastMutation() : this.lastMutation, + generation: generation ?? this.generation, + activeAccount: activeAccount != null ? activeAccount() : this.activeAccount, + authenticatedSessions: authenticatedSessions ?? this.authenticatedSessions, + anonymousSessions: anonymousSessions ?? this.anonymousSessions, + error: error != null ? error() : this.error, + ); + } + + @override + List get props => [status, mutationStatus, lastMutation, generation, activeAccount, authenticatedSessions, anonymousSessions, error]; +} diff --git a/lib/src/features/session/presentation/utils/effective_account_context.dart b/lib/src/features/session/presentation/utils/effective_account_context.dart new file mode 100644 index 000000000..e6baf1e88 --- /dev/null +++ b/lib/src/features/session/presentation/utils/effective_account_context.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:thunder/src/features/account/account.dart'; +import 'package:thunder/src/features/session/presentation/state/feature_account_cubit.dart'; +import 'package:thunder/src/features/session/presentation/state/session_bloc.dart'; + +class CapturedAccountContext { + const CapturedAccountContext({ + required this.effectiveAccount, + this.sessionBloc, + this.profileBloc, + this.featureAccountCubit, + }); + + final Account effectiveAccount; + final SessionBloc? sessionBloc; + final ProfileBloc? profileBloc; + final FeatureAccountCubit? featureAccountCubit; + + factory CapturedAccountContext.capture(BuildContext context, {Account? fallbackAccount}) { + SessionBloc? sessionBloc; + ProfileBloc? profileBloc; + FeatureAccountCubit? featureAccountCubit; + + try { + sessionBloc = context.read(); + } catch (_) { + sessionBloc = null; + } + + try { + profileBloc = context.read(); + } catch (_) { + profileBloc = null; + } + + try { + featureAccountCubit = context.read(); + } catch (_) { + featureAccountCubit = null; + } + + return CapturedAccountContext( + effectiveAccount: resolveEffectiveAccount(context, fallbackAccount: fallbackAccount), + sessionBloc: sessionBloc, + profileBloc: profileBloc, + featureAccountCubit: featureAccountCubit, + ); + } + + Widget wrap(Widget child) { + return MultiBlocProvider( + providers: [ + if (sessionBloc != null) BlocProvider.value(value: sessionBloc!), + if (profileBloc != null) BlocProvider.value(value: profileBloc!), + if (featureAccountCubit != null) + BlocProvider.value(value: featureAccountCubit!) + else + BlocProvider(create: (_) => FeatureAccountCubit(baseAccount: effectiveAccount)), + ], + child: child, + ); + } +} + +Widget wrapWithCapturedAccountContext(BuildContext context, Widget child, {Account? fallbackAccount}) { + return CapturedAccountContext.capture(context, fallbackAccount: fallbackAccount).wrap(child); +} + +Account resolveActiveAccount(BuildContext context, {Account? fallbackAccount}) { + try { + final activeAccount = context.read().state.activeAccount; + if (activeAccount != null) { + return activeAccount; + } + } catch (_) { + // Fall through to older account providers while the app finishes migrating. + } + + if (fallbackAccount != null) { + return fallbackAccount; + } + + try { + return context.read().state.account; + } catch (_) { + throw StateError('No active account available in the current widget tree.'); + } +} + +Account resolveEffectiveAccount(BuildContext context, {Account? fallbackAccount}) { + try { + return context.read().state.effectiveAccount; + } catch (_) { + return resolveActiveAccount(context, fallbackAccount: fallbackAccount); + } +} diff --git a/lib/src/features/session/session.dart b/lib/src/features/session/session.dart new file mode 100644 index 000000000..37a2a1eb4 --- /dev/null +++ b/lib/src/features/session/session.dart @@ -0,0 +1,9 @@ +export 'domain/repositories/session_repository.dart'; +export 'domain/models/feature_account.dart'; +export 'domain/models/feature_account_resolution_request.dart'; +export 'domain/models/feature_account_resolved_content.dart'; +export 'domain/services/feature_account_content_resolver.dart'; +export 'data/repositories/persistent_session_repository.dart'; +export 'presentation/utils/effective_account_context.dart'; +export 'presentation/state/feature_account_cubit.dart'; +export 'presentation/state/session_bloc.dart'; diff --git a/lib/src/features/settings/presentation/pages/accessibility_settings_page.dart b/lib/src/features/settings/presentation/pages/accessibility_settings_page.dart index 36c309d5a..0bd749285 100644 --- a/lib/src/features/settings/presentation/pages/accessibility_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/accessibility_settings_page.dart @@ -42,7 +42,7 @@ class _AccessibilitySettingsPageState extends State w } if (context.mounted) { - context.read().add(UserPreferencesChangeEvent()); + context.read().reload(); } } diff --git a/lib/src/features/settings/presentation/pages/appearance/comment_appearance_settings_page.dart b/lib/src/features/settings/presentation/pages/appearance/comment_appearance_settings_page.dart index ab36513cd..dd62615b0 100644 --- a/lib/src/features/settings/presentation/pages/appearance/comment_appearance_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/appearance/comment_appearance_settings_page.dart @@ -103,7 +103,7 @@ class _CommentAppearanceSettingsPageState extends State().add(UserPreferencesChangeEvent()); + context.read().reload(); context.read().reload(); } } @@ -122,7 +122,7 @@ class _CommentAppearanceSettingsPageState extends State().add(UserPreferencesChangeEvent()); + context.read().reload(); context.read().reload(); } } diff --git a/lib/src/features/settings/presentation/pages/appearance/post_appearance_settings_page.dart b/lib/src/features/settings/presentation/pages/appearance/post_appearance_settings_page.dart index 0b95ef10e..58dfa01a6 100644 --- a/lib/src/features/settings/presentation/pages/appearance/post_appearance_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/appearance/post_appearance_settings_page.dart @@ -305,7 +305,7 @@ class _PostAppearanceSettingsPageState extends State } if (context.mounted) { - context.read().add(UserPreferencesChangeEvent()); + context.read().reload(); context.read().reload(); } } @@ -347,7 +347,7 @@ class _PostAppearanceSettingsPageState extends State await initPreferences(); if (context.mounted) { - context.read().add(UserPreferencesChangeEvent()); + context.read().reload(); context.read().reload(); } } diff --git a/lib/src/features/settings/presentation/pages/appearance/theme_settings_page.dart b/lib/src/features/settings/presentation/pages/appearance/theme_settings_page.dart index cea0d1358..b9f69bd92 100644 --- a/lib/src/features/settings/presentation/pages/appearance/theme_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/appearance/theme_settings_page.dart @@ -308,7 +308,7 @@ class _ThemeSettingsPageState extends State { } if (context.mounted) { - context.read().add(UserPreferencesChangeEvent()); + context.read().reload(); context.read().reload(); } } diff --git a/lib/src/features/settings/presentation/pages/behavior/fab_settings_page.dart b/lib/src/features/settings/presentation/pages/behavior/fab_settings_page.dart index 8e0ce432b..4447f7286 100644 --- a/lib/src/features/settings/presentation/pages/behavior/fab_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/behavior/fab_settings_page.dart @@ -162,7 +162,7 @@ class _FabSettingsPage extends State with TickerProviderStateMi } if (context.mounted) { - context.read().add(UserPreferencesChangeEvent()); + context.read().reload(); context.read().reload(); } } diff --git a/lib/src/features/settings/presentation/pages/behavior/filter_settings_page.dart b/lib/src/features/settings/presentation/pages/behavior/filter_settings_page.dart index 829877563..4b3413724 100644 --- a/lib/src/features/settings/presentation/pages/behavior/filter_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/behavior/filter_settings_page.dart @@ -46,7 +46,7 @@ class _FilterSettingsPageState extends State with SingleTick } if (context.mounted) { - context.read().add(UserPreferencesChangeEvent()); + context.read().reload(); context.read().reload(); } } diff --git a/lib/src/features/settings/presentation/pages/behavior/general_settings_page.dart b/lib/src/features/settings/presentation/pages/behavior/general_settings_page.dart index 78f457659..6e34211f1 100644 --- a/lib/src/features/settings/presentation/pages/behavior/general_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/behavior/general_settings_page.dart @@ -257,7 +257,7 @@ class _GeneralSettingsPageState extends State with SingleTi } if (context.mounted) { - context.read().add(UserPreferencesChangeEvent()); + context.read().reload(); context.read().reload(); } } @@ -998,7 +998,7 @@ class _GeneralSettingsPageState extends State with SingleTi if (context.mounted) { _initPreferences(); - context.read().add(UserPreferencesChangeEvent()); + context.read().reload(); context.read().reload(); } else { showSnackbar(l10n.settingsNotImportedSuccessfully); diff --git a/lib/src/features/settings/presentation/pages/behavior/gesture_settings_page.dart b/lib/src/features/settings/presentation/pages/behavior/gesture_settings_page.dart index 5ec53dc69..980a19274 100644 --- a/lib/src/features/settings/presentation/pages/behavior/gesture_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/behavior/gesture_settings_page.dart @@ -143,7 +143,7 @@ class _GestureSettingsPageState extends State with TickerPr } if (context.mounted) { - context.read().add(UserPreferencesChangeEvent()); + context.read().reload(); context.read().reload(); } } diff --git a/lib/src/features/settings/presentation/pages/debug_settings_page.dart b/lib/src/features/settings/presentation/pages/debug_settings_page.dart index 13ae094cf..e3bf3b5bb 100644 --- a/lib/src/features/settings/presentation/pages/debug_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/debug_settings_page.dart @@ -22,6 +22,7 @@ import 'package:thunder/src/foundation/utils/utils.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:unifiedpush/unifiedpush.dart'; import 'package:thunder/packages/ui/ui.dart'; import 'package:thunder/src/features/settings/presentation/utils/setting_link_utils.dart'; @@ -73,6 +74,10 @@ class _DebugSettingsPageState extends State { default: break; } + + if (mounted) { + BlocProvider.of(super.context).reload(); + } } @override @@ -200,7 +205,7 @@ class _DebugSettingsPageState extends State { final cleared = await UserPreferences.clearAllPreferences(); if (cleared) { - context.read().add(UserPreferencesChangeEvent()); + context.read().reload(); showSnackbar(AppLocalizations.of(context)!.clearedUserPreferences); } else { showSnackbar(AppLocalizations.of(context)!.failedToPerformAction); @@ -230,8 +235,6 @@ class _DebugSettingsPageState extends State { onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), secondaryButtonText: l10n.cancel, onPrimaryButtonPressed: (dialogContext, _) async { - String path = join(await getDatabasesPath(), 'thunder.db'); - final dbFolder = await getApplicationDocumentsDirectory(); final file = File(join(dbFolder.path, 'thunder.sqlite')); @@ -416,7 +419,7 @@ class _DebugSettingsPageState extends State { ), onTap: inboxNotificationType == NotificationType.unifiedPush ? () async { - final error = await requestTestNotification(); + final error = await requestTestNotification(resolveActiveAccount(context)); if (error == null) { showSnackbar(l10n.sentRequestForTestNotification); @@ -455,7 +458,7 @@ class _DebugSettingsPageState extends State { ); if (result) { - final error = await requestTestNotification(); + final error = await requestTestNotification(resolveActiveAccount(context)); if (error == null) { showSnackbar(l10n.sentRequestForTestNotification); diff --git a/lib/src/features/settings/presentation/widgets/discussion_language_selector.dart b/lib/src/features/settings/presentation/widgets/discussion_language_selector.dart index 3f1cc2547..385239c27 100644 --- a/lib/src/features/settings/presentation/widgets/discussion_language_selector.dart +++ b/lib/src/features/settings/presentation/widgets/discussion_language_selector.dart @@ -5,10 +5,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; +import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/shared/input_dialogs.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/foundation/config/config.dart'; -import 'package:thunder/packages/ui/ui.dart' show showThunderDialog; +import 'package:thunder/packages/ui/ui.dart' show showSnackbar, showThunderDialog; class DiscussionLanguageSelector extends StatefulWidget { const DiscussionLanguageSelector({super.key}); @@ -18,35 +19,35 @@ class DiscussionLanguageSelector extends StatefulWidget { } class _DiscussionLanguageSelector extends State { - List _languages = []; - - @override - void initState() { - super.initState(); - - final state = context.read().state; - setState(() => _languages = state.siteResponse?.allLanguages ?? []); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); final l10n = AppLocalizations.of(context)!; - return BlocBuilder( + return BlocConsumer( + listener: (context, state) { + if (state.status == AccountSettingsStatus.failure) { + showSnackbar(state.errorMessage ?? l10n.unexpectedError); + } else if (state.status == AccountSettingsStatus.success) { + context.read().add(FetchProfileSettings()); + } + }, builder: (context, state) { + final languages = state.siteResponse?.allLanguages ?? const []; final selectedLanguages = state.siteResponse?.myUser?.discussionLanguages ?? []; - final discussionLanguages = selectedLanguages.map((id) => _languages.firstWhere((language) => language.id == id)).toList(); + final discussionLanguages = selectedLanguages.map((id) => languages.firstWhere((language) => language.id == id)).toList(); return Scaffold( floatingActionButton: FloatingActionButton( onPressed: () => showLanguageInputDialog( context, title: l10n.addDiscussionLanguage, + account: context.read().state.account, excludedLanguageIds: [-1], + suggestions: languages, onLanguageSelected: (language) { List updatedDiscussionLanguages = List.from(discussionLanguages)..add(language); - context.read().add(UpdateUserSettingsEvent(discussionLanguages: updatedDiscussionLanguages.map((e) => e.id).toList())); + context.read().updateSettings(discussionLanguages: updatedDiscussionLanguages.map((e) => e.id).toList()); }, ), child: const Icon(Icons.add_rounded), @@ -89,7 +90,7 @@ class _DiscussionLanguageSelector extends State { primaryButtonText: l10n.remove, onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) { final updatedDiscussionLanguages = discussionLanguages.where((element) => element != discussionLanguages[index]).toList(); - context.read().add(UpdateUserSettingsEvent(discussionLanguages: updatedDiscussionLanguages.map((e) => e.id).toList())); + context.read().updateSettings(discussionLanguages: updatedDiscussionLanguages.map((e) => e.id).toList()); Navigator.of(dialogContext).pop(); }, secondaryButtonText: l10n.cancel, @@ -97,7 +98,7 @@ class _DiscussionLanguageSelector extends State { ); } else { final updatedDiscussionLanguages = discussionLanguages.where((element) => element != discussionLanguages[index]).toList(); - context.read().add(UpdateUserSettingsEvent(discussionLanguages: updatedDiscussionLanguages.map((e) => e.id).toList())); + context.read().updateSettings(discussionLanguages: updatedDiscussionLanguages.map((e) => e.id).toList()); } }, ), diff --git a/lib/src/features/user/presentation/pages/media_management_page.dart b/lib/src/features/user/presentation/pages/media_management_page.dart index 3223b7d14..d30677744 100644 --- a/lib/src/features/user/presentation/pages/media_management_page.dart +++ b/lib/src/features/user/presentation/pages/media_management_page.dart @@ -22,7 +22,9 @@ import 'package:thunder/src/shared/content/utils/media/media_utils.dart'; import 'package:thunder/packages/ui/ui.dart' show showSnackbar, showThunderDialog; class MediaManagementPage extends StatelessWidget { - const MediaManagementPage({super.key}); + const MediaManagementPage({super.key, required this.account}); + + final Account account; @override Widget build(BuildContext context) { @@ -31,18 +33,19 @@ class MediaManagementPage extends StatelessWidget { final dateFormat = context.select((cubit) => cubit.state.dateFormat); final metadataFontSizeScale = context.select((cubit) => cubit.state.metadataFontSizeScale); - final imageCachingMode = context.select((cubit) => cubit.state.imageCachingMode); + final imageCachingMode = context.select((cubit) => cubit.state.imageCachingMode); - return BlocBuilder( - builder: (context, state) { - if (state.status == UserSettingsStatus.failedListingMedia && state.errorMessage?.isNotEmpty == true) { + return BlocConsumer( + listener: (context, state) { + if (state.status == UserMediaStatus.loadFailure && state.errorMessage?.isNotEmpty == true) { showSnackbar( state.errorMessage!, trailingIcon: Icons.refresh_rounded, - trailingAction: () => context.read().add(const ListMediaEvent()), + trailingAction: () => context.read().loadMedia(), ); } - + }, + builder: (context, state) { return Scaffold( body: Container( color: theme.colorScheme.surface, @@ -58,24 +61,21 @@ class MediaManagementPage extends StatelessWidget { l10n.manageMedia, style: theme.textTheme.titleLarge, ), - subtitle: UserFullNameWidget( - name: context.read().state.account.username, - displayName: context.read().state.account.displayName, - instance: context.read().state.account.instance), + subtitle: UserFullNameWidget(name: account.username, displayName: account.displayName, instance: account.instance), contentPadding: const EdgeInsets.symmetric(horizontal: 0), ), ), - if (state.status == UserSettingsStatus.listingMedia) + if (state.status == UserMediaStatus.loading) const SliverFillRemaining( child: Center( child: CircularProgressIndicator(), ), ), - if (state.status == UserSettingsStatus.searchingMedia || - state.status == UserSettingsStatus.succeededSearchingMedia || - state.status == UserSettingsStatus.deletingMedia || - state.status == UserSettingsStatus.failedListingMedia || - state.status == UserSettingsStatus.succeededListingMedia) ...[ + if (state.status == UserMediaStatus.searching || + state.status == UserMediaStatus.searchSuccess || + state.status == UserMediaStatus.deleting || + state.status == UserMediaStatus.loadFailure || + state.status == UserMediaStatus.loadSuccess) ...[ if (state.images?.isNotEmpty == true) SliverList.builder( addSemanticIndexes: false, @@ -83,7 +83,6 @@ class MediaManagementPage extends StatelessWidget { addRepaintBoundaries: false, itemCount: state.images!.length, itemBuilder: (context, index) { - final account = context.read().state.account; String url = 'https://${account.instance}/pictrs/image/${state.images![index]['local_image']['pictrs_alias']}'; return KeepAlive( @@ -152,8 +151,8 @@ class MediaManagementPage extends StatelessWidget { const Spacer(), IconButton( onPressed: () async { - final UserSettingsBloc userSettingsBloc = context.read(); - userSettingsBloc.add(FindMediaUsagesEvent(id: state.images![index]['local_image']['pictrs_alias'])); + final UserMediaCubit userMediaCubit = context.read(); + userMediaCubit.findMediaUsages(id: state.images![index]['local_image']['pictrs_alias']); showModalBottomSheet( context: context, @@ -163,26 +162,24 @@ class MediaManagementPage extends StatelessWidget { return AnimatedSize( duration: const Duration(milliseconds: 250), child: BlocProvider.value( - value: userSettingsBloc, - child: BlocBuilder( + value: userMediaCubit, + child: BlocBuilder( builder: (context, state) { - if (state.status == UserSettingsStatus.failedListingMedia) { + if (state.status == UserMediaStatus.loadFailure) { Navigator.of(context).pop(); } - final account = context.read().state.account; - return SingleChildScrollView( child: Column( children: [ - if (state.status == UserSettingsStatus.searchingMedia) + if (state.status == UserMediaStatus.searching) const SizedBox( height: 200, child: Center( child: CircularProgressIndicator(), ), ) - else if (state.status == UserSettingsStatus.succeededSearchingMedia) ...[ + else if (state.status == UserMediaStatus.searchSuccess) ...[ if (state.imageSearchPosts?.isNotEmpty == true) BlocProvider.value( value: createFeedBloc(account), @@ -208,7 +205,7 @@ class MediaManagementPage extends StatelessWidget { itemBuilder: (context, index) => CommentListEntry(comment: state.imageSearchComments![index]), ), ], - if (state.status == UserSettingsStatus.succeededSearchingMedia && + if (state.status == UserMediaStatus.searchSuccess && state.imageSearchComments?.isNotEmpty != true && state.imageSearchPosts?.isNotEmpty != true) SizedBox( @@ -227,7 +224,7 @@ class MediaManagementPage extends StatelessWidget { ), ), ), - if (state.status == UserSettingsStatus.succeededSearchingMedia && + if (state.status == UserMediaStatus.searchSuccess && (state.imageSearchComments?.isNotEmpty == true || state.imageSearchPosts?.isNotEmpty == true)) const SizedBox(height: 50), ], @@ -262,8 +259,9 @@ class MediaManagementPage extends StatelessWidget { ); if (result && context.mounted) { - context.read().add( - DeleteMediaEvent(deleteToken: state.images![index]['local_image']['pictrs_delete_token'], id: state.images![index]['local_image']['pictrs_alias'])); + context + .read() + .deleteMedia(deleteToken: state.images![index]['local_image']['pictrs_delete_token'], id: state.images![index]['local_image']['pictrs_alias']); } }, icon: const Icon(Icons.delete_forever), diff --git a/lib/src/features/user/presentation/pages/user_settings_block_page.dart b/lib/src/features/user/presentation/pages/user_settings_block_page.dart index b09771757..b32b34431 100644 --- a/lib/src/features/user/presentation/pages/user_settings_block_page.dart +++ b/lib/src/features/user/presentation/pages/user_settings_block_page.dart @@ -34,7 +34,6 @@ class _UserSettingsBlockPageState extends State with Sing void initState() { super.initState(); tabController = TabController(vsync: this, length: 3); - context.read().add(const GetUserBlocksEvent()); } @override @@ -43,7 +42,7 @@ class _UserSettingsBlockPageState extends State with Sing super.dispose(); } - List getPersonBlocks(BuildContext context, UserSettingsState state, List persons) { + List getPersonBlocks(BuildContext context, UserBlocksState state, List persons) { final l10n = AppLocalizations.of(context)!; return persons.map((person) { @@ -66,7 +65,7 @@ class _UserSettingsBlockPageState extends State with Sing useDisplayName: false, ), leading: UserAvatar(user: person), - trailing: state.status == UserSettingsStatus.blocking && state.personBeingBlocked == person.id + trailing: state.status == UserBlocksStatus.blocking && state.personBeingBlocked == person.id ? const Padding( padding: EdgeInsets.only(right: 12), child: SizedBox( @@ -77,7 +76,7 @@ class _UserSettingsBlockPageState extends State with Sing ) : IconButton( icon: Icon(Icons.clear, semanticLabel: l10n.remove), - onPressed: () => context.read().add(UnblockPersonEvent(personId: person.id)), + onPressed: () => context.read().unblockPerson(personId: person.id), ), onTap: () { navigateToFeedPage(context, feedType: FeedType.user, username: "${person.name}@${fetchInstanceNameFromUrl(person.actorId)}"); @@ -87,7 +86,7 @@ class _UserSettingsBlockPageState extends State with Sing }).toList(); } - List getCommunityBlocks(BuildContext context, UserSettingsState state, List communities) { + List getCommunityBlocks(BuildContext context, UserBlocksState state, List communities) { final l10n = AppLocalizations.of(context)!; return communities.map((community) { @@ -110,7 +109,7 @@ class _UserSettingsBlockPageState extends State with Sing useDisplayName: false, ), leading: CommunityAvatar(community: community, radius: 16.0), - trailing: state.status == UserSettingsStatus.blocking && state.communityBeingBlocked == community.id + trailing: state.status == UserBlocksStatus.blocking && state.communityBeingBlocked == community.id ? const Padding( padding: EdgeInsets.only(right: 12), child: SizedBox( @@ -121,7 +120,7 @@ class _UserSettingsBlockPageState extends State with Sing ) : IconButton( icon: Icon(Icons.clear, semanticLabel: l10n.remove), - onPressed: () => context.read().add(UnblockCommunityEvent(communityId: community.id)), + onPressed: () => context.read().unblockCommunity(communityId: community.id), ), onTap: () { navigateToFeedPage(context, feedType: FeedType.community, communityName: "${community.name}@${fetchInstanceNameFromUrl(community.actorId)}"); @@ -131,7 +130,7 @@ class _UserSettingsBlockPageState extends State with Sing }).toList(); } - List getInstanceBlocks(BuildContext context, UserSettingsState state, List> instances) { + List getInstanceBlocks(BuildContext context, UserBlocksState state, List> instances) { final l10n = AppLocalizations.of(context)!; final theme = Theme.of(context); @@ -155,7 +154,7 @@ class _UserSettingsBlockPageState extends State with Sing ), ), ), - trailing: state.status == UserSettingsStatus.blocking && state.instanceBeingBlocked == instance['id'] + trailing: state.status == UserBlocksStatus.blocking && state.instanceBeingBlocked == instance['id'] ? const Padding( padding: EdgeInsets.only(right: 12), child: SizedBox( @@ -166,7 +165,7 @@ class _UserSettingsBlockPageState extends State with Sing ) : IconButton( icon: Icon(Icons.clear, semanticLabel: l10n.remove), - onPressed: () => context.read().add(UnblockInstanceEvent(instanceId: instance['id'])), + onPressed: () => context.read().unblockInstance(instanceId: instance['id']), ), onTap: () { navigateToInstancePage(context, instanceHost: instance['domain'], instanceId: instance['id']); @@ -190,7 +189,7 @@ class _UserSettingsBlockPageState extends State with Sing title: l10n.blockUser, account: context.read().state.account, onUserSelected: (user) { - context.read().add(UnblockPersonEvent(personId: user.id, unblock: false)); + context.read().unblockPerson(personId: user.id, unblock: false); }, ); break; @@ -200,7 +199,7 @@ class _UserSettingsBlockPageState extends State with Sing title: l10n.blockCommunity, account: context.read().state.account, onCommunitySelected: (ThunderCommunity community) { - context.read().add(UnblockCommunityEvent(communityId: community.id, unblock: false)); + context.read().unblockCommunity(communityId: community.id, unblock: false); }, ); break; @@ -208,8 +207,9 @@ class _UserSettingsBlockPageState extends State with Sing showInstanceInputDialog( context, title: l10n.blockInstance, + account: context.read().state.account, onInstanceSelected: (instanceWithFederationState) { - context.read().add(UnblockInstanceEvent(instanceId: instanceWithFederationState.id!, unblock: false)); + context.read().unblockInstance(instanceId: instanceWithFederationState.id!, unblock: false); }, ); break; @@ -219,31 +219,31 @@ class _UserSettingsBlockPageState extends State with Sing }, child: const Icon(Icons.add_rounded), ), - body: BlocConsumer( + body: BlocConsumer( listener: (context, state) { bool isBlock = (state.personBeingBlocked != 0 || state.communityBeingBlocked != 0 || state.instanceBeingBlocked != 0); - if (state.status == UserSettingsStatus.failure && !isBlock) { + if (state.status == UserBlocksStatus.failure && !isBlock) { return showSnackbar(state.errorMessage ?? l10n.unexpectedError); } - if (state.status == UserSettingsStatus.failure) { + if (state.status == UserBlocksStatus.failure) { showSnackbar(l10n.failedToUnblock(state.errorMessage ?? l10n.missingErrorMessage)); - } else if (state.status == UserSettingsStatus.failedRevert) { + } else if (state.status == UserBlocksStatus.failedRevert) { showSnackbar(l10n.failedToBlock(state.errorMessage ?? l10n.missingErrorMessage)); - } else if (state.status == UserSettingsStatus.revert) { + } else if (state.status == UserBlocksStatus.revert) { showSnackbar(l10n.successfullyBlocked); - } else if (state.status == UserSettingsStatus.successBlock) { + } else if (state.status == UserBlocksStatus.successBlock) { showSnackbar( l10n.successfullyUnblocked, trailingIcon: Icons.undo_rounded, trailingAction: () { if (state.personBeingBlocked != 0) { - context.read().add(UnblockPersonEvent(personId: state.personBeingBlocked, unblock: false)); + context.read().unblockPerson(personId: state.personBeingBlocked, unblock: false); } else if (state.communityBeingBlocked != 0) { - context.read().add(UnblockCommunityEvent(communityId: state.communityBeingBlocked, unblock: false)); + context.read().unblockCommunity(communityId: state.communityBeingBlocked, unblock: false); } else if (state.instanceBeingBlocked != 0) { - context.read().add(UnblockInstanceEvent(instanceId: state.instanceBeingBlocked, unblock: false)); + context.read().unblockInstance(instanceId: state.instanceBeingBlocked, unblock: false); } }, ); @@ -305,7 +305,7 @@ class _UserSettingsBlockPageState extends State with Sing /// This class creates a widget which displays a list of items. If no items are available, it displays a message. class UserSettingBlockList extends StatelessWidget { /// The status of the bloc - final UserSettingsStatus status; + final UserBlocksStatus status; /// The text to display if no items are available final String? emptyText; @@ -324,7 +324,7 @@ class UserSettingBlockList extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); - if (status == UserSettingsStatus.initial) { + if (status == UserBlocksStatus.initial || status == UserBlocksStatus.loading) { return Container( margin: const EdgeInsets.all(10.0), child: const Center(child: CircularProgressIndicator()), diff --git a/lib/src/features/user/presentation/pages/user_settings_page.dart b/lib/src/features/user/presentation/pages/user_settings_page.dart index c63c58aa8..33755ec7e 100644 --- a/lib/src/features/user/presentation/pages/user_settings_page.dart +++ b/lib/src/features/user/presentation/pages/user_settings_page.dart @@ -56,7 +56,6 @@ class _UserSettingsPageState extends State { @override void initState() { super.initState(); - context.read().add(const GetUserSettingsEvent()); if (widget.settingToHighlight != null) { setState(() => settingToHighlight = widget.settingToHighlight); @@ -103,15 +102,17 @@ class _UserSettingsPageState extends State { body: SafeArea( top: false, child: BlocListener( + listenWhen: (_, state) => state.status == ProfileStatus.success && state.siteResponse != null, listener: (context, state) { if (!context.mounted) return; - context.read().add(const ResetUserSettingsEvent()); - context.read().add(const GetUserSettingsEvent()); + context.read().hydrateFromProfile(state.siteResponse); }, - child: BlocConsumer( + child: BlocConsumer( listener: (context, state) { - if (state.status == UserSettingsStatus.failure) { + if (state.status == AccountSettingsStatus.failure) { showSnackbar(state.errorMessage ?? l10n.unexpectedError); + } else if (state.status == AccountSettingsStatus.success) { + context.read().add(FetchProfileSettings()); } }, builder: (context, state) { @@ -122,7 +123,7 @@ class _UserSettingsPageState extends State { ThunderUser? person = myUser?.localUserView.person; return CustomScrollView( - physics: state.status == UserSettingsStatus.notLoggedIn ? const NeverScrollableScrollPhysics() : null, + physics: state.status == AccountSettingsStatus.notLoggedIn ? const NeverScrollableScrollPhysics() : null, slivers: [ SliverAppBar( pinned: true, @@ -138,8 +139,8 @@ class _UserSettingsPageState extends State { ], ), switch (state.status) { - UserSettingsStatus.notLoggedIn => const SliverFillRemaining(hasScrollBody: false, child: AccountPlaceholder()), - UserSettingsStatus.initial => const SliverFillRemaining( + AccountSettingsStatus.notLoggedIn => const SliverFillRemaining(hasScrollBody: false, child: AccountPlaceholder()), + AccountSettingsStatus.initial => const SliverFillRemaining( hasScrollBody: false, child: Center( child: CircularProgressIndicator(), @@ -191,7 +192,7 @@ class _UserSettingsPageState extends State { ), primaryButtonText: l10n.save, onPrimaryButtonPressed: (dialogContext, _) { - context.read().add(UpdateUserSettingsEvent(displayName: displayNameTextController.text)); + context.read().updateSettings(displayName: displayNameTextController.text); Navigator.of(dialogContext).pop(); }, secondaryButtonText: l10n.cancel, @@ -224,7 +225,7 @@ class _UserSettingsPageState extends State { ), primaryButtonText: l10n.save, onPrimaryButtonPressed: (dialogContext, _) { - context.read().add(UpdateUserSettingsEvent(bio: bioTextController.text)); + context.read().updateSettings(bio: bioTextController.text); Navigator.of(dialogContext).pop(); }, secondaryButtonText: l10n.cancel, @@ -251,7 +252,7 @@ class _UserSettingsPageState extends State { ), primaryButtonText: l10n.save, onPrimaryButtonPressed: (dialogContext, _) { - context.read().add(UpdateUserSettingsEvent(email: emailTextController.text)); + context.read().updateSettings(email: emailTextController.text); Navigator.of(dialogContext).pop(); }, secondaryButtonText: l10n.cancel, @@ -277,7 +278,7 @@ class _UserSettingsPageState extends State { ), primaryButtonText: l10n.save, onPrimaryButtonPressed: (dialogContext, _) { - context.read().add(UpdateUserSettingsEvent(matrixUserId: matrixUserTextController.text)); + context.read().updateSettings(matrixUserId: matrixUserTextController.text); Navigator.of(dialogContext).pop(); }, secondaryButtonText: l10n.cancel, @@ -310,7 +311,7 @@ class _UserSettingsPageState extends State { ListPickerItem(icon: Icons.grid_view_rounded, label: FeedListType.local.value, payload: FeedListType.local), ], leading: Icon(Icons.filter_alt_rounded), - onChanged: (value) async => context.read().add(UpdateUserSettingsEvent(defaultFeedListType: value.payload)), + onChanged: (value) async => context.read().updateSettings(defaultFeedListType: value.payload), highlightKey: settingToHighlightKey, onLongPress: () => shareLocalSetting(context, LocalSettings.accountDefaultFeedType), highlighted: settingToHighlight == LocalSettings.accountDefaultFeedType), @@ -329,7 +330,7 @@ class _UserSettingsPageState extends State { account: account, title: l10n.defaultFeedSortType, onSelect: (value) async { - context.read().add(UpdateUserSettingsEvent(defaultPostSortType: value.payload)); + context.read().updateSettings(defaultPostSortType: value.payload); }, previouslySelected: localUser?.defaultSortType, ), @@ -351,7 +352,7 @@ class _UserSettingsPageState extends State { value: localUser?.showNsfw, iconEnabled: Icons.no_adult_content, iconDisabled: Icons.no_adult_content, - onChanged: (bool value) => context.read().add(UpdateUserSettingsEvent(showNsfw: value)), + onChanged: (bool value) => context.read().updateSettings(showNsfw: value), highlightKey: settingToHighlightKey, onLongPress: () => shareLocalSetting(context, LocalSettings.accountShowNsfwContent), highlighted: settingToHighlight == LocalSettings.accountShowNsfwContent), @@ -360,7 +361,7 @@ class _UserSettingsPageState extends State { value: localUser?.showScores, iconEnabled: Icons.onetwothree_rounded, iconDisabled: Icons.onetwothree_rounded, - onChanged: (bool value) => {context.read().add(UpdateUserSettingsEvent(showScores: value))}, + onChanged: (bool value) => {context.read().updateSettings(showScores: value)}, highlightKey: settingToHighlightKey, onLongPress: () => shareLocalSetting(context, LocalSettings.accountShowScores), highlighted: settingToHighlight == LocalSettings.accountShowScores), @@ -369,7 +370,7 @@ class _UserSettingsPageState extends State { value: localUser?.showReadPosts, iconEnabled: Icons.fact_check_rounded, iconDisabled: Icons.fact_check_outlined, - onChanged: (bool value) => {context.read().add(UpdateUserSettingsEvent(showReadPosts: value))}, + onChanged: (bool value) => {context.read().updateSettings(showReadPosts: value)}, highlightKey: settingToHighlightKey, onLongPress: () => shareLocalSetting(context, LocalSettings.accountShowReadPosts), highlighted: settingToHighlight == LocalSettings.accountShowReadPosts), @@ -379,7 +380,7 @@ class _UserSettingsPageState extends State { iconEnabled: Thunder.robot, iconDisabled: Thunder.robot, iconSpacing: 14.0, - onChanged: (bool value) => {context.read().add(UpdateUserSettingsEvent(botAccount: value))}, + onChanged: (bool value) => {context.read().updateSettings(botAccount: value)}, highlightKey: settingToHighlightKey, onLongPress: () => shareLocalSetting(context, LocalSettings.accountIsBot), highlighted: settingToHighlight == LocalSettings.accountIsBot), @@ -389,7 +390,7 @@ class _UserSettingsPageState extends State { iconEnabled: Thunder.robot, iconDisabled: Thunder.robot, iconSpacing: 14.0, - onChanged: (bool value) => {context.read().add(UpdateUserSettingsEvent(showBotAccounts: value))}, + onChanged: (bool value) => {context.read().updateSettings(showBotAccounts: value)}, highlightKey: settingToHighlightKey, onLongPress: () => shareLocalSetting(context, LocalSettings.accountShowBotAccounts), highlighted: settingToHighlight == LocalSettings.accountShowBotAccounts), @@ -505,10 +506,7 @@ class _UserSettingsPageState extends State { if (success) { showSnackbar(l10n.accountSettingsImportedSuccessfully); - - // Reload the current page we're on to reflect changes to account settings - context.read().add(const ResetUserSettingsEvent()); - context.read().add(const GetUserSettingsEvent()); + context.read().add(FetchProfileSettings()); } else { showSnackbar(l10n.errorImportingAccountSettings); } diff --git a/lib/src/features/user/presentation/state/account_settings_cubit.dart b/lib/src/features/user/presentation/state/account_settings_cubit.dart new file mode 100644 index 000000000..71c6a2812 --- /dev/null +++ b/lib/src/features/user/presentation/state/account_settings_cubit.dart @@ -0,0 +1,174 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +import 'package:thunder/src/features/account/account.dart'; +import 'package:thunder/src/foundation/contracts/contracts.dart'; +import 'package:thunder/src/foundation/errors/errors.dart'; +import 'package:thunder/src/foundation/networking/networking.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; + +const _accountSettingsUnset = Object(); + +enum AccountSettingsStatus { + initial, + ready, + updating, + success, + failure, + notLoggedIn, +} + +class AccountSettingsState extends Equatable { + const AccountSettingsState({ + this.status = AccountSettingsStatus.initial, + this.siteResponse, + this.errorMessage = '', + this.errorReason, + }); + + final AccountSettingsStatus status; + final ThunderSiteResponse? siteResponse; + final String? errorMessage; + final AppErrorReason? errorReason; + + AccountSettingsState copyWith({ + AccountSettingsStatus? status, + Object? siteResponse = _accountSettingsUnset, + Object? errorMessage = _accountSettingsUnset, + Object? errorReason = _accountSettingsUnset, + }) { + return AccountSettingsState( + status: status ?? this.status, + siteResponse: identical(siteResponse, _accountSettingsUnset) ? this.siteResponse : siteResponse as ThunderSiteResponse?, + errorMessage: identical(errorMessage, _accountSettingsUnset) ? this.errorMessage : errorMessage as String?, + errorReason: identical(errorReason, _accountSettingsUnset) ? this.errorReason : errorReason as AppErrorReason?, + ); + } + + @override + List get props => [status, siteResponse, errorMessage, errorReason]; +} + +class AccountSettingsCubit extends Cubit { + AccountSettingsCubit({required this.account, required this.accountRepository, required LocalizationService localizationService, ThunderSiteResponse? initialSiteResponse}) + : _localizationService = localizationService, + super( + account.anonymous + ? AccountSettingsState( + status: AccountSettingsStatus.notLoggedIn, + errorMessage: localizationService.l10n.userNotLoggedIn, + errorReason: AppErrorReason.notLoggedIn(message: localizationService.l10n.userNotLoggedIn), + ) + : AccountSettingsState( + status: initialSiteResponse == null ? AccountSettingsStatus.initial : AccountSettingsStatus.ready, + siteResponse: initialSiteResponse, + ), + ); + + final Account account; + final AccountRepository accountRepository; + final LocalizationService _localizationService; + + void hydrateFromProfile(ThunderSiteResponse? siteResponse) { + if (account.anonymous || siteResponse == null) return; + + emit( + state.copyWith( + status: AccountSettingsStatus.ready, + siteResponse: siteResponse, + errorMessage: '', + errorReason: null, + ), + ); + } + + Future updateSettings({ + String? displayName, + String? bio, + String? email, + String? matrixUserId, + FeedListType? defaultFeedListType, + PostSortType? defaultPostSortType, + bool? showNsfw, + bool? showReadPosts, + bool? showScores, + bool? botAccount, + bool? showBotAccounts, + List? discussionLanguages, + }) async { + final originalSiteResponse = state.siteResponse; + + try { + final l10n = _localizationService.l10n; + + if (account.anonymous) { + return emit(state.copyWith( + status: AccountSettingsStatus.notLoggedIn, + errorMessage: l10n.userNotLoggedIn, + errorReason: AppErrorReason.notLoggedIn(message: l10n.userNotLoggedIn), + )); + } + + if (originalSiteResponse?.myUser == null) { + return emit(state.copyWith( + status: AccountSettingsStatus.failure, + errorMessage: l10n.unexpectedError, + errorReason: AppErrorReason.validation(message: l10n.unexpectedError), + )); + } + + final localUser = originalSiteResponse!.myUser!.localUserView.localUser.copyWith( + email: email ?? originalSiteResponse.myUser!.localUserView.localUser.email, + showReadPosts: showReadPosts ?? originalSiteResponse.myUser!.localUserView.localUser.showReadPosts, + showScores: showScores ?? originalSiteResponse.myUser!.localUserView.localUser.showScores, + showBotAccounts: showBotAccounts ?? originalSiteResponse.myUser!.localUserView.localUser.showBotAccounts, + showNsfw: showNsfw ?? originalSiteResponse.myUser!.localUserView.localUser.showNsfw, + defaultListingType: defaultFeedListType ?? originalSiteResponse.myUser!.localUserView.localUser.defaultListingType, + defaultSortType: defaultPostSortType ?? originalSiteResponse.myUser!.localUserView.localUser.defaultSortType, + ); + + final updatedSiteResponse = originalSiteResponse.copyWith( + myUser: originalSiteResponse.myUser!.copyWith( + localUserView: originalSiteResponse.myUser!.localUserView.copyWith( + person: originalSiteResponse.myUser!.localUserView.person.copyWith( + botAccount: botAccount ?? originalSiteResponse.myUser!.localUserView.person.botAccount, + bio: bio ?? originalSiteResponse.myUser!.localUserView.person.bio, + displayName: displayName ?? originalSiteResponse.myUser!.localUserView.person.displayName, + matrixUserId: matrixUserId ?? originalSiteResponse.myUser!.localUserView.person.matrixUserId, + ), + localUser: localUser, + ), + discussionLanguages: discussionLanguages ?? originalSiteResponse.myUser!.discussionLanguages, + ), + ); + + emit(state.copyWith(status: AccountSettingsStatus.ready, siteResponse: updatedSiteResponse, errorMessage: '', errorReason: null)); + emit(state.copyWith(status: AccountSettingsStatus.updating, errorMessage: '', errorReason: null)); + + await accountRepository.saveSettings( + bio: bio, + email: email, + matrixUserId: matrixUserId, + displayName: displayName, + defaultFeedListType: defaultFeedListType, + defaultPostSortType: defaultPostSortType, + showNsfw: showNsfw, + showReadPosts: showReadPosts, + showScores: showScores, + botAccount: botAccount, + showBotAccounts: showBotAccounts, + discussionLanguages: discussionLanguages, + ); + + emit(state.copyWith(status: AccountSettingsStatus.success, errorMessage: '', errorReason: null)); + } catch (e) { + final message = getExceptionErrorMessage(e); + emit(state.copyWith( + status: AccountSettingsStatus.failure, + siteResponse: originalSiteResponse, + errorMessage: message, + errorReason: AppErrorReason.unexpected(message: message, details: e.toString()), + )); + } + } +} diff --git a/lib/src/features/user/presentation/state/user_blocks_cubit.dart b/lib/src/features/user/presentation/state/user_blocks_cubit.dart new file mode 100644 index 000000000..0eb432ab3 --- /dev/null +++ b/lib/src/features/user/presentation/state/user_blocks_cubit.dart @@ -0,0 +1,208 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +import 'package:thunder/src/features/community/community.dart'; +import 'package:thunder/src/features/instance/instance.dart'; +import 'package:thunder/src/features/user/data/repositories/user_repository.dart'; +import 'package:thunder/src/foundation/contracts/contracts.dart'; +import 'package:thunder/src/foundation/errors/errors.dart'; +import 'package:thunder/src/foundation/networking/networking.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; + +const _userBlocksUnset = Object(); + +enum UserBlocksStatus { + initial, + loading, + success, + blocking, + successBlock, + failure, + revert, + failedRevert, + notLoggedIn, +} + +class UserBlocksState extends Equatable { + const UserBlocksState({ + this.status = UserBlocksStatus.initial, + this.personBlocks = const [], + this.communityBlocks = const [], + this.instanceBlocks = const [], + this.personBeingBlocked = 0, + this.communityBeingBlocked = 0, + this.instanceBeingBlocked = 0, + this.errorMessage = '', + this.errorReason, + }); + + final UserBlocksStatus status; + final List personBlocks; + final List communityBlocks; + final List> instanceBlocks; + final int personBeingBlocked; + final int communityBeingBlocked; + final int instanceBeingBlocked; + final String? errorMessage; + final AppErrorReason? errorReason; + + UserBlocksState copyWith({ + UserBlocksStatus? status, + List? personBlocks, + List? communityBlocks, + List>? instanceBlocks, + int? personBeingBlocked, + int? communityBeingBlocked, + int? instanceBeingBlocked, + Object? errorMessage = _userBlocksUnset, + Object? errorReason = _userBlocksUnset, + }) { + return UserBlocksState( + status: status ?? this.status, + personBlocks: personBlocks ?? this.personBlocks, + communityBlocks: communityBlocks ?? this.communityBlocks, + instanceBlocks: instanceBlocks ?? this.instanceBlocks, + personBeingBlocked: personBeingBlocked ?? this.personBeingBlocked, + communityBeingBlocked: communityBeingBlocked ?? this.communityBeingBlocked, + instanceBeingBlocked: instanceBeingBlocked ?? this.instanceBeingBlocked, + errorMessage: identical(errorMessage, _userBlocksUnset) ? this.errorMessage : errorMessage as String?, + errorReason: identical(errorReason, _userBlocksUnset) ? this.errorReason : errorReason as AppErrorReason?, + ); + } + + @override + List get props => [status, personBlocks, communityBlocks, instanceBlocks, personBeingBlocked, communityBeingBlocked, instanceBeingBlocked, errorMessage, errorReason]; +} + +class UserBlocksCubit extends Cubit { + UserBlocksCubit({ + required this.account, + required this.instanceRepository, + required this.communityRepository, + required this.userRepository, + required LocalizationService localizationService, + }) : _localizationService = localizationService, + super(const UserBlocksState()); + + final Account account; + final InstanceRepository instanceRepository; + final CommunityRepository communityRepository; + final UserRepository userRepository; + final LocalizationService _localizationService; + + Future loadBlocks() async { + try { + final l10n = _localizationService.l10n; + + if (account.anonymous) { + return emit(state.copyWith( + status: UserBlocksStatus.notLoggedIn, + errorMessage: l10n.userNotLoggedIn, + errorReason: AppErrorReason.notLoggedIn(message: l10n.userNotLoggedIn), + )); + } + + emit(state.copyWith(status: UserBlocksStatus.loading, errorMessage: '', errorReason: null)); + + final getSiteResponse = await instanceRepository.info(); + final personBlocks = getSiteResponse.myUser!.personBlocks..sort((a, b) => a.name.compareTo(b.name)); + final communityBlocks = getSiteResponse.myUser!.communityBlocks..sort((a, b) => a.name.compareTo(b.name)); + final instanceBlocks = getSiteResponse.myUser!.instanceBlocks.map((instanceBlockView) => instanceBlockView.instance).toList()..sort((a, b) => a['domain'].compareTo(b['domain'])); + + emit(state.copyWith( + status: state.instanceBeingBlocked != 0 && instanceBlocks.any((instance) => instance['id'] == state.instanceBeingBlocked) ? UserBlocksStatus.revert : UserBlocksStatus.success, + personBlocks: personBlocks, + communityBlocks: communityBlocks, + instanceBlocks: instanceBlocks, + errorMessage: '', + errorReason: null, + )); + } catch (e) { + final message = getExceptionErrorMessage(e); + emit(state.copyWith( + status: UserBlocksStatus.failure, + errorMessage: message, + errorReason: AppErrorReason.unexpected(message: message, details: e.toString()), + )); + } + } + + Future unblockInstance({required int instanceId, bool unblock = true}) async { + emit(state.copyWith(status: UserBlocksStatus.blocking, instanceBeingBlocked: instanceId, personBeingBlocked: 0, communityBeingBlocked: 0)); + + try { + await instanceRepository.block(instanceId, !unblock); + emit(state.copyWith(instanceBeingBlocked: instanceId, personBeingBlocked: 0, communityBeingBlocked: 0, errorMessage: '', errorReason: null)); + await loadBlocks(); + } catch (e) { + final message = getExceptionErrorMessage(e); + emit(state.copyWith( + status: unblock ? UserBlocksStatus.failure : UserBlocksStatus.failedRevert, + errorMessage: message, + errorReason: AppErrorReason.actionFailed(message: message, details: e.toString()), + )); + } + } + + Future unblockCommunity({required int communityId, bool unblock = true}) async { + try { + final l10n = _localizationService.l10n; + + if (account.anonymous) { + return emit(state.copyWith( + status: UserBlocksStatus.notLoggedIn, + errorMessage: l10n.userNotLoggedIn, + errorReason: AppErrorReason.notLoggedIn(message: l10n.userNotLoggedIn), + )); + } + + emit(state.copyWith(status: UserBlocksStatus.blocking, communityBeingBlocked: communityId, personBeingBlocked: 0, instanceBeingBlocked: 0)); + + final community = await communityRepository.block(communityId, !unblock); + final updatedCommunityBlocks = unblock ? state.communityBlocks.where((community) => community.id != communityId).toList() : [...state.communityBlocks, community]; + updatedCommunityBlocks.sort((a, b) => a.name.compareTo(b.name)); + + emit(state.copyWith( + status: unblock ? UserBlocksStatus.successBlock : UserBlocksStatus.revert, + communityBlocks: updatedCommunityBlocks, + communityBeingBlocked: communityId, + personBeingBlocked: 0, + errorMessage: '', + errorReason: null, + )); + } catch (e) { + final message = getExceptionErrorMessage(e); + emit(state.copyWith( + status: unblock ? UserBlocksStatus.failure : UserBlocksStatus.failedRevert, + errorMessage: message, + errorReason: AppErrorReason.actionFailed(message: message, details: e.toString()), + )); + } + } + + Future unblockPerson({required int personId, bool unblock = true}) async { + emit(state.copyWith(status: UserBlocksStatus.blocking, personBeingBlocked: personId, communityBeingBlocked: 0, instanceBeingBlocked: 0)); + + try { + final user = await userRepository.block(personId, !unblock); + final updatedPersonBlocks = unblock ? state.personBlocks.where((person) => person.id != personId).toList() : [...state.personBlocks, user]; + updatedPersonBlocks.sort((a, b) => a.name.compareTo(b.name)); + + emit(state.copyWith( + status: unblock ? UserBlocksStatus.successBlock : UserBlocksStatus.revert, + personBlocks: updatedPersonBlocks, + personBeingBlocked: personId, + communityBeingBlocked: 0, + errorMessage: '', + errorReason: null, + )); + } catch (e) { + final message = getExceptionErrorMessage(e); + emit(state.copyWith( + status: unblock ? UserBlocksStatus.failure : UserBlocksStatus.failedRevert, + errorMessage: message, + errorReason: AppErrorReason.actionFailed(message: message, details: e.toString()), + )); + } + } +} diff --git a/lib/src/features/user/presentation/state/user_media_cubit.dart b/lib/src/features/user/presentation/state/user_media_cubit.dart new file mode 100644 index 000000000..7a25b7430 --- /dev/null +++ b/lib/src/features/user/presentation/state/user_media_cubit.dart @@ -0,0 +1,144 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +import 'package:thunder/src/features/account/account.dart'; +import 'package:thunder/src/features/post/post.dart'; +import 'package:thunder/src/features/search/search.dart'; +import 'package:thunder/src/features/user/domain/utils/user_media_utils.dart'; +import 'package:thunder/src/foundation/contracts/contracts.dart'; +import 'package:thunder/src/foundation/errors/errors.dart'; +import 'package:thunder/src/foundation/networking/networking.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; + +const _userMediaUnset = Object(); + +enum UserMediaStatus { + initial, + loading, + loadSuccess, + loadFailure, + deleting, + searching, + searchSuccess, + notLoggedIn, +} + +class UserMediaState extends Equatable { + const UserMediaState({ + this.status = UserMediaStatus.initial, + this.errorMessage = '', + this.errorReason, + this.images, + this.imageSearchPosts, + this.imageSearchComments, + }); + + final UserMediaStatus status; + final String? errorMessage; + final AppErrorReason? errorReason; + final List>? images; + final List? imageSearchPosts; + final List? imageSearchComments; + + UserMediaState copyWith({ + UserMediaStatus? status, + Object? errorMessage = _userMediaUnset, + Object? errorReason = _userMediaUnset, + Object? images = _userMediaUnset, + Object? imageSearchPosts = _userMediaUnset, + Object? imageSearchComments = _userMediaUnset, + }) { + return UserMediaState( + status: status ?? this.status, + errorMessage: identical(errorMessage, _userMediaUnset) ? this.errorMessage : errorMessage as String?, + errorReason: identical(errorReason, _userMediaUnset) ? this.errorReason : errorReason as AppErrorReason?, + images: identical(images, _userMediaUnset) ? this.images : images as List>?, + imageSearchPosts: identical(imageSearchPosts, _userMediaUnset) ? this.imageSearchPosts : imageSearchPosts as List?, + imageSearchComments: identical(imageSearchComments, _userMediaUnset) ? this.imageSearchComments : imageSearchComments as List?, + ); + } + + @override + List get props => [status, errorMessage, errorReason, images, imageSearchPosts, imageSearchComments]; +} + +class UserMediaCubit extends Cubit { + UserMediaCubit({required this.account, required this.accountRepository, required this.searchRepository, required LocalizationService localizationService}) + : _localizationService = localizationService, + super(const UserMediaState()); + + final Account account; + final AccountRepository accountRepository; + final SearchRepository searchRepository; + final LocalizationService _localizationService; + + Future loadMedia() async { + emit(state.copyWith(status: UserMediaStatus.loading, errorMessage: '', errorReason: null)); + + try { + int page = 1; + final images = >[]; + + while (true) { + final response = await accountRepository.media(page: page); + if (response.isEmpty) break; + + images.addAll(response.images); + page++; + } + + emit(state.copyWith(status: UserMediaStatus.loadSuccess, images: images, errorMessage: '', errorReason: null)); + } catch (e) { + final message = getExceptionErrorMessage(e); + emit(state.copyWith(status: UserMediaStatus.loadFailure, errorMessage: message, errorReason: AppErrorReason.unexpected(message: message, details: e.toString()))); + } + } + + Future deleteMedia({required String deleteToken, required String id}) async { + emit(state.copyWith(status: UserMediaStatus.deleting, errorMessage: '', errorReason: null)); + + try { + final images = removeImageByAlias(images: state.images ?? const [], alias: id); + final l10n = _localizationService.l10n; + + if (account.anonymous) { + return emit(state.copyWith( + status: UserMediaStatus.notLoggedIn, + errorMessage: l10n.userNotLoggedIn, + errorReason: AppErrorReason.notLoggedIn(message: l10n.userNotLoggedIn), + )); + } + + await accountRepository.deleteImage(file: id, token: deleteToken); + emit(state.copyWith(status: UserMediaStatus.loadSuccess, images: images, errorMessage: '', errorReason: null)); + } catch (e) { + final message = _localizationService.l10n.errorDeletingImage(getExceptionErrorMessage(e)); + emit(state.copyWith(status: UserMediaStatus.loadFailure, errorMessage: message, errorReason: AppErrorReason.actionFailed(message: message, details: e.toString()))); + } + } + + Future findMediaUsages({required String id}) async { + emit(state.copyWith(status: UserMediaStatus.searching, errorMessage: '', errorReason: null, imageSearchPosts: null, imageSearchComments: null)); + + try { + final url = Uri.https(account.instance, 'pictrs/image/$id').toString(); + + final postsResponse = await searchRepository.search(query: url, type: MetaSearchType.posts); + final postsByUrlResponse = await searchRepository.search(query: url, type: MetaSearchType.url); + final response = await searchRepository.search(query: url, type: MetaSearchType.comments); + + final posts = mergeUniquePosts(primary: postsResponse.posts, secondary: postsByUrlResponse.posts); + + emit(state.copyWith( + status: UserMediaStatus.searchSuccess, + imageSearchPosts: await parsePosts(posts), + imageSearchComments: response.comments, + errorMessage: '', + errorReason: null, + )); + } catch (e) { + final message = getExceptionErrorMessage(e); + emit(state.copyWith(status: UserMediaStatus.loadFailure, errorMessage: message, errorReason: AppErrorReason.unexpected(message: message, details: e.toString()))); + } + } +} diff --git a/lib/src/features/user/presentation/state/user_settings_bloc.dart b/lib/src/features/user/presentation/state/user_settings_bloc.dart deleted file mode 100644 index 6f6c7f2a2..000000000 --- a/lib/src/features/user/presentation/state/user_settings_bloc.dart +++ /dev/null @@ -1,480 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:bloc_concurrency/bloc_concurrency.dart'; -import 'package:equatable/equatable.dart'; -import 'package:stream_transform/stream_transform.dart'; - -import 'package:thunder/src/features/community/community.dart'; -import 'package:thunder/src/foundation/contracts/contracts.dart'; -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/foundation/errors/errors.dart'; -import 'package:thunder/src/features/instance/instance.dart'; -import 'package:thunder/src/features/account/account.dart'; -import 'package:thunder/src/features/post/post.dart'; -import 'package:thunder/src/features/search/search.dart'; -import 'package:thunder/src/features/user/user.dart'; -import 'package:thunder/src/features/user/domain/utils/user_media_utils.dart'; -import 'package:thunder/src/foundation/networking/networking.dart'; - -part 'user_settings_event.dart'; -part 'user_settings_state.dart'; - -const throttleDuration = Duration(seconds: 1); -const timeout = Duration(seconds: 5); - -EventTransformer throttleDroppable(Duration duration) { - return (events, mapper) => droppable().call(events.throttle(duration), mapper); -} - -class UserSettingsBloc extends Bloc { - final Account account; - - final InstanceRepository instanceRepository; - final SearchRepository searchRepository; - final CommunityRepository communityRepository; - final AccountRepository accountRepository; - final UserRepository userRepository; - final ActiveAccountProvider _activeAccountProvider; - final LocalizationService _localizationService; - - UserSettingsBloc({ - required this.account, - required this.instanceRepository, - required this.searchRepository, - required this.communityRepository, - required this.accountRepository, - required this.userRepository, - required ActiveAccountProvider activeAccountProvider, - required LocalizationService localizationService, - }) : _activeAccountProvider = activeAccountProvider, - _localizationService = localizationService, - super(const UserSettingsState()) { - on( - _resetUserSettingsEvent, - transformer: throttleDroppable(throttleDuration), - ); - on( - _getUserSettingsEvent, - transformer: throttleDroppable(throttleDuration), - ); - on( - _updateUserSettingsEvent, - transformer: throttleDroppable(throttleDuration), - ); - on( - _getUserBlocksEvent, - transformer: throttleDroppable(throttleDuration), - ); - on( - _unblockInstanceEvent, - transformer: throttleDroppable(throttleDuration), - ); - on( - _unblockCommunityEvent, - transformer: throttleDroppable(throttleDuration), - ); - on( - _unblockPersonEvent, - transformer: throttleDroppable(throttleDuration), - ); - on( - _listMediaEvent, - transformer: throttleDroppable(throttleDuration), - ); - on( - _deleteMediaEvent, - // Do not use any transformer, because a throttleDroppable will only process the first request and restartable will only process the last. - ); - on( - _findMediaUsagesEvent, - ); - } - - Future _resetUserSettingsEvent(ResetUserSettingsEvent event, emit) async { - return emit( - state.copyWith( - status: UserSettingsStatus.initial, - errorMessage: '', - errorReason: null, - ), - ); - } - - Future _getUserSettingsEvent(GetUserSettingsEvent event, emit) async { - try { - final l10n = _localizationService.l10n; - final account = await _activeAccountProvider.getActiveAccount(); - if (account.anonymous) { - return emit(state.copyWith( - status: UserSettingsStatus.notLoggedIn, - errorMessage: l10n.userNotLoggedIn, - errorReason: AppErrorReason.notLoggedIn(message: l10n.userNotLoggedIn), - )); - } - - final getSiteResponse = await instanceRepository.info(); - - return emit( - state.copyWith( - status: UserSettingsStatus.success, - siteResponse: getSiteResponse, - errorReason: null, - ), - ); - } catch (e) { - final message = getExceptionErrorMessage(e); - return emit(state.copyWith( - status: UserSettingsStatus.failure, - errorMessage: message, - errorReason: AppErrorReason.unexpected( - message: message, - details: e.toString(), - ), - )); - } - } - - Future _updateUserSettingsEvent(UpdateUserSettingsEvent event, emit) async { - final originalGetSiteResponse = state.siteResponse; - try { - final l10n = _localizationService.l10n; - final account = await _activeAccountProvider.getActiveAccount(); - if (account.anonymous) { - return emit(state.copyWith( - status: UserSettingsStatus.notLoggedIn, - errorMessage: l10n.userNotLoggedIn, - errorReason: AppErrorReason.notLoggedIn(message: l10n.userNotLoggedIn), - )); - } - - if (originalGetSiteResponse == null) { - return emit(state.copyWith( - status: UserSettingsStatus.failure, - errorMessage: l10n.unexpectedError, - errorReason: AppErrorReason.validation( - message: l10n.unexpectedError, - ), - )); - } - - // Optimistically update settings - ThunderLocalUser localUser = state.siteResponse!.myUser!.localUserView.localUser.copyWith( - email: event.email ?? state.siteResponse!.myUser!.localUserView.localUser.email, - showReadPosts: event.showReadPosts ?? state.siteResponse!.myUser!.localUserView.localUser.showReadPosts, - showScores: event.showScores ?? state.siteResponse!.myUser!.localUserView.localUser.showScores, - showBotAccounts: event.showBotAccounts ?? state.siteResponse!.myUser!.localUserView.localUser.showBotAccounts, - showNsfw: event.showNsfw ?? state.siteResponse!.myUser!.localUserView.localUser.showNsfw, - defaultListingType: event.defaultFeedListType ?? state.siteResponse!.myUser!.localUserView.localUser.defaultListingType, - defaultSortType: event.defaultPostSortType ?? state.siteResponse!.myUser!.localUserView.localUser.defaultSortType, - ); - - ThunderSiteResponse updatedGetSiteResponse = state.siteResponse!.copyWith( - myUser: state.siteResponse!.myUser!.copyWith( - localUserView: state.siteResponse!.myUser!.localUserView.copyWith( - person: state.siteResponse!.myUser!.localUserView.person.copyWith( - botAccount: event.botAccount ?? state.siteResponse!.myUser!.localUserView.person.botAccount, - bio: event.bio ?? state.siteResponse!.myUser!.localUserView.person.bio, - displayName: event.displayName ?? state.siteResponse!.myUser!.localUserView.person.displayName, - matrixUserId: event.matrixUserId ?? state.siteResponse!.myUser!.localUserView.person.matrixUserId, - ), - localUser: localUser, - ), - discussionLanguages: event.discussionLanguages ?? state.siteResponse!.discussionLanguages, - ), - ); - - emit(state.copyWith( - status: UserSettingsStatus.success, - siteResponse: updatedGetSiteResponse, - errorReason: null, - )); - emit(state.copyWith(status: UserSettingsStatus.updating, errorReason: null)); - - await accountRepository.saveSettings( - bio: event.bio, - email: event.email, - matrixUserId: event.matrixUserId, - displayName: event.displayName, - defaultFeedListType: event.defaultFeedListType, - defaultPostSortType: event.defaultPostSortType, - showNsfw: event.showNsfw, - showReadPosts: event.showReadPosts, - showScores: event.showScores, - botAccount: event.botAccount, - showBotAccounts: event.showBotAccounts, - discussionLanguages: event.discussionLanguages, - ); - - return emit(state.copyWith(status: UserSettingsStatus.success, errorReason: null)); - } catch (e) { - final message = getExceptionErrorMessage(e); - return emit(state.copyWith( - status: UserSettingsStatus.failure, - siteResponse: originalGetSiteResponse, - errorMessage: message, - errorReason: AppErrorReason.unexpected( - message: message, - details: e.toString(), - ), - )); - } - } - - Future _getUserBlocksEvent(GetUserBlocksEvent event, emit) async { - try { - final l10n = _localizationService.l10n; - final account = await _activeAccountProvider.getActiveAccount(); - if (account.anonymous) { - return emit(state.copyWith( - status: UserSettingsStatus.notLoggedIn, - errorMessage: l10n.userNotLoggedIn, - errorReason: AppErrorReason.notLoggedIn(message: l10n.userNotLoggedIn), - )); - } - - final getSiteResponse = await instanceRepository.info(); - - final personBlocks = getSiteResponse.myUser!.personBlocks..sort((a, b) => a.name.compareTo(b.name)); - final communityBlocks = getSiteResponse.myUser!.communityBlocks..sort((a, b) => a.name.compareTo(b.name)); - final instanceBlocks = getSiteResponse.myUser!.instanceBlocks.map((instanceBlockView) => instanceBlockView.instance).toList()..sort((a, b) => a['domain'].compareTo(b['domain'])); - - return emit(state.copyWith( - status: (state.instanceBeingBlocked != 0 && instanceBlocks.any((instance) => instance['id'] == state.instanceBeingBlocked)) ? UserSettingsStatus.revert : UserSettingsStatus.success, - personBlocks: personBlocks, - communityBlocks: communityBlocks, - instanceBlocks: instanceBlocks, - errorReason: null, - )); - } catch (e) { - final message = getExceptionErrorMessage(e); - return emit(state.copyWith( - status: UserSettingsStatus.failure, - errorMessage: message, - errorReason: AppErrorReason.unexpected( - message: message, - details: e.toString(), - ), - )); - } - } - - Future _unblockInstanceEvent(UnblockInstanceEvent event, emit) async { - emit(state.copyWith(status: UserSettingsStatus.blocking, instanceBeingBlocked: event.instanceId, personBeingBlocked: 0, communityBeingBlocked: 0)); - - try { - await instanceRepository.block(event.instanceId, !event.unblock); - - emit(state.copyWith( - status: state.status, - instanceBeingBlocked: event.instanceId, - personBeingBlocked: 0, - communityBeingBlocked: 0, - )); - - return add(const GetUserBlocksEvent()); - } catch (e) { - final message = getExceptionErrorMessage(e); - return emit(state.copyWith( - status: event.unblock ? UserSettingsStatus.failure : UserSettingsStatus.failedRevert, - errorMessage: message, - errorReason: AppErrorReason.actionFailed( - message: message, - details: e.toString(), - ), - )); - } - } - - Future _unblockCommunityEvent(UnblockCommunityEvent event, emit) async { - try { - final l10n = _localizationService.l10n; - final account = await _activeAccountProvider.getActiveAccount(); - if (account.anonymous) { - return emit(state.copyWith( - status: UserSettingsStatus.notLoggedIn, - errorMessage: l10n.userNotLoggedIn, - errorReason: AppErrorReason.notLoggedIn(message: l10n.userNotLoggedIn), - )); - } - - emit(state.copyWith(status: UserSettingsStatus.blocking, communityBeingBlocked: event.communityId, personBeingBlocked: 0, instanceBeingBlocked: 0)); - - final community = await communityRepository.block(event.communityId, !event.unblock); - - List updatedCommunityBlocks; - if (event.unblock) { - updatedCommunityBlocks = state.communityBlocks.where((community) => community.id != event.communityId).toList()..sort((a, b) => a.name.compareTo(b.name)); - } else { - updatedCommunityBlocks = (state.communityBlocks + [community])..sort((a, b) => a.name.compareTo(b.name)); - } - - return emit(state.copyWith( - status: event.unblock ? UserSettingsStatus.successBlock : UserSettingsStatus.revert, - communityBlocks: updatedCommunityBlocks, - communityBeingBlocked: event.communityId, - personBeingBlocked: 0, - errorReason: null, - )); - } catch (e) { - final message = getExceptionErrorMessage(e); - return emit(state.copyWith( - status: event.unblock ? UserSettingsStatus.failure : UserSettingsStatus.failedRevert, - errorMessage: message, - errorReason: AppErrorReason.actionFailed( - message: message, - details: e.toString(), - ), - )); - } - } - - Future _unblockPersonEvent(UnblockPersonEvent event, emit) async { - emit(state.copyWith(status: UserSettingsStatus.blocking, personBeingBlocked: event.personId, communityBeingBlocked: 0, instanceBeingBlocked: 0)); - - try { - final user = await userRepository.block(event.personId, !event.unblock); - - List updatedPersonBlocks; - if (event.unblock) { - updatedPersonBlocks = state.personBlocks.where((person) => person.id != event.personId).toList()..sort((a, b) => a.name.compareTo(b.name)); - } else { - updatedPersonBlocks = (state.personBlocks + [user])..sort((a, b) => a.name.compareTo(b.name)); - } - - return emit(state.copyWith( - status: event.unblock ? UserSettingsStatus.successBlock : UserSettingsStatus.revert, - personBlocks: updatedPersonBlocks, - personBeingBlocked: event.personId, - communityBeingBlocked: 0, - errorReason: null, - )); - } catch (e) { - final message = getExceptionErrorMessage(e); - return emit(state.copyWith( - status: event.unblock ? UserSettingsStatus.failure : UserSettingsStatus.failedRevert, - errorMessage: message, - errorReason: AppErrorReason.actionFailed( - message: message, - details: e.toString(), - ), - )); - } - } - - Future _listMediaEvent(ListMediaEvent event, emit) async { - emit(state.copyWith(status: UserSettingsStatus.listingMedia)); - - try { - int page = 1; - final images = >[]; - - while (true) { - final response = await accountRepository.media(page: page); - if (response.isEmpty) break; - - images.addAll(response.images); - page++; - } - - return emit(state.copyWith( - status: UserSettingsStatus.succeededListingMedia, - images: images, - errorReason: null, - )); - } catch (e) { - final message = getExceptionErrorMessage(e); - return emit(state.copyWith( - status: UserSettingsStatus.failedListingMedia, - errorMessage: message, - errorReason: AppErrorReason.unexpected( - message: message, - details: e.toString(), - ), - )); - } - } - - Future _deleteMediaEvent(DeleteMediaEvent event, emit) async { - emit(state.copyWith(status: UserSettingsStatus.deletingMedia)); - - try { - // Optimistically remove the media from the list - final images = removeImageByAlias( - images: state.images ?? const [], - alias: event.id, - ); - - final l10n = _localizationService.l10n; - final account = await _activeAccountProvider.getActiveAccount(); - if (account.anonymous) { - return emit(state.copyWith( - status: UserSettingsStatus.notLoggedIn, - errorMessage: l10n.userNotLoggedIn, - errorReason: AppErrorReason.notLoggedIn(message: l10n.userNotLoggedIn), - )); - } - - await accountRepository.deleteImage(file: event.id, token: event.deleteToken); - - return emit(state.copyWith( - status: UserSettingsStatus.succeededListingMedia, - images: images, - errorReason: null, - )); - } catch (e) { - final message = _localizationService.l10n.errorDeletingImage(getExceptionErrorMessage(e)); - return emit( - state.copyWith( - status: UserSettingsStatus.failedListingMedia, - errorMessage: message, - errorReason: AppErrorReason.actionFailed( - message: message, - details: e.toString(), - ), - ), - ); - } - } - - Future _findMediaUsagesEvent(FindMediaUsagesEvent event, emit) async { - emit(state.copyWith(status: UserSettingsStatus.searchingMedia)); - - try { - final account = await _activeAccountProvider.getActiveAccount(); - String url = Uri.https(account.instance, 'pictrs/image/${event.id}').toString(); - - final postsResponse = await searchRepository.search(query: url, type: MetaSearchType.posts); - final postsByUrlResponse = await searchRepository.search(query: url, type: MetaSearchType.url); - - List posts = postsResponse.posts; - List postsByUrl = postsByUrlResponse.posts; - - // De-dup posts found by body and URL - posts = mergeUniquePosts( - primary: posts, - secondary: postsByUrl, - ); - - final response = await searchRepository.search(query: url, type: MetaSearchType.comments); - final List comments = response.comments; - - return emit(state.copyWith( - status: UserSettingsStatus.succeededSearchingMedia, - imageSearchPosts: await parsePosts(posts), - imageSearchComments: comments, - errorReason: null, - )); - } catch (e) { - final message = getExceptionErrorMessage(e); - return emit( - state.copyWith( - status: UserSettingsStatus.failedListingMedia, - errorMessage: message, - errorReason: AppErrorReason.unexpected( - message: message, - details: e.toString(), - ), - ), - ); - } - } -} diff --git a/lib/src/features/user/presentation/state/user_settings_event.dart b/lib/src/features/user/presentation/state/user_settings_event.dart deleted file mode 100644 index 7204aefbb..000000000 --- a/lib/src/features/user/presentation/state/user_settings_event.dart +++ /dev/null @@ -1,129 +0,0 @@ -part of 'user_settings_bloc.dart'; - -abstract class UserSettingsEvent extends Equatable { - const UserSettingsEvent(); - - @override - List get props => []; -} - -class ResetUserSettingsEvent extends UserSettingsEvent { - const ResetUserSettingsEvent(); -} - -class GetUserSettingsEvent extends UserSettingsEvent { - const GetUserSettingsEvent(); -} - -class UpdateUserSettingsEvent extends UserSettingsEvent { - /// The display name associated with the user - final String? displayName; - - /// The profile bio associated with the user - final String? bio; - - /// The email associated with the user - final String? email; - - /// The matrix user id associated with the user - final String? matrixUserId; - - /// The default listing type for the feed - final FeedListType? defaultFeedListType; - - /// The default sort type for the feed - final PostSortType? defaultPostSortType; - - /// Whether or not NSFW content should be shown - final bool? showNsfw; - - /// Whether or not read posts should be shown - final bool? showReadPosts; - - /// Whether or not post/comment scores should be shown - final bool? showScores; - - /// Whether the current user is a bot - final bool? botAccount; - - /// Whether or not bot accounts should be shown - final bool? showBotAccounts; - - /// The languages associated with the user - final List? discussionLanguages; - - const UpdateUserSettingsEvent({ - this.displayName, - this.bio, - this.email, - this.matrixUserId, - this.defaultFeedListType, - this.defaultPostSortType, - this.showNsfw, - this.showReadPosts, - this.showScores, - this.botAccount, - this.showBotAccounts, - this.discussionLanguages, - }); - - @override - List get props => [displayName, bio, email, matrixUserId, defaultFeedListType, defaultPostSortType, showNsfw, showReadPosts, showScores, botAccount, showBotAccounts, discussionLanguages]; -} - -class GetUserBlocksEvent extends UserSettingsEvent { - const GetUserBlocksEvent(); -} - -class UnblockInstanceEvent extends UserSettingsEvent { - final int instanceId; - final bool unblock; - - const UnblockInstanceEvent({required this.instanceId, this.unblock = true}); - - @override - List get props => [instanceId, unblock]; -} - -class UnblockCommunityEvent extends UserSettingsEvent { - final int communityId; - final bool unblock; - - const UnblockCommunityEvent({required this.communityId, this.unblock = true}); - - @override - List get props => [communityId, unblock]; -} - -class UnblockPersonEvent extends UserSettingsEvent { - final int personId; - final bool unblock; - - const UnblockPersonEvent({required this.personId, this.unblock = true}); - - @override - List get props => [personId, unblock]; -} - -class ListMediaEvent extends UserSettingsEvent { - const ListMediaEvent(); -} - -class DeleteMediaEvent extends UserSettingsEvent { - final String deleteToken; - final String id; - - const DeleteMediaEvent({required this.deleteToken, required this.id}); - - @override - List get props => [deleteToken, id]; -} - -class FindMediaUsagesEvent extends UserSettingsEvent { - final String id; - - const FindMediaUsagesEvent({required this.id}); - - @override - List get props => [id]; -} diff --git a/lib/src/features/user/presentation/state/user_settings_state.dart b/lib/src/features/user/presentation/state/user_settings_state.dart deleted file mode 100644 index f7df0e8bd..000000000 --- a/lib/src/features/user/presentation/state/user_settings_state.dart +++ /dev/null @@ -1,106 +0,0 @@ -part of 'user_settings_bloc.dart'; - -const _userSettingsUnset = Object(); - -enum UserSettingsStatus { - initial, - updating, - success, - blocking, - successBlock, - failure, - revert, - failedRevert, - notLoggedIn, - listingMedia, - failedListingMedia, - succeededListingMedia, - deletingMedia, - searchingMedia, - succeededSearchingMedia, -} - -class UserSettingsState extends Equatable { - const UserSettingsState({ - this.status = UserSettingsStatus.initial, - this.personBlocks = const [], - this.communityBlocks = const [], - this.instanceBlocks = const [], - this.personBeingBlocked = 0, - this.communityBeingBlocked = 0, - this.instanceBeingBlocked = 0, - this.siteResponse, - this.errorMessage = '', - this.errorReason, - this.images, - this.imageSearchPosts, - this.imageSearchComments, - }); - - final UserSettingsStatus status; - - final List personBlocks; - final List communityBlocks; - final List> instanceBlocks; - - final int personBeingBlocked; - final int communityBeingBlocked; - final int instanceBeingBlocked; - - final ThunderSiteResponse? siteResponse; - - final String? errorMessage; - final AppErrorReason? errorReason; - final List>? images; - final List? imageSearchPosts; - final List? imageSearchComments; - - UserSettingsState copyWith({ - UserSettingsStatus? status, - List? personBlocks, - List? communityBlocks, - List>? instanceBlocks, - int? personBeingBlocked, - int? communityBeingBlocked, - int? instanceBeingBlocked, - Object? siteResponse = _userSettingsUnset, - Object? errorMessage = _userSettingsUnset, - Object? errorReason = _userSettingsUnset, - Object? images = _userSettingsUnset, - Object? imageSearchPosts = _userSettingsUnset, - Object? imageSearchComments = _userSettingsUnset, - }) { - return UserSettingsState( - status: status ?? this.status, - personBlocks: personBlocks ?? this.personBlocks, - communityBlocks: communityBlocks ?? this.communityBlocks, - instanceBlocks: instanceBlocks ?? this.instanceBlocks, - personBeingBlocked: personBeingBlocked ?? this.personBeingBlocked, - communityBeingBlocked: communityBeingBlocked ?? this.communityBeingBlocked, - instanceBeingBlocked: instanceBeingBlocked ?? this.instanceBeingBlocked, - siteResponse: identical(siteResponse, _userSettingsUnset) ? this.siteResponse : siteResponse as ThunderSiteResponse?, - errorMessage: identical(errorMessage, _userSettingsUnset) ? this.errorMessage : errorMessage as String?, - errorReason: identical(errorReason, _userSettingsUnset) ? this.errorReason : errorReason as AppErrorReason?, - images: identical(images, _userSettingsUnset) ? this.images : images as List>?, - imageSearchPosts: identical(imageSearchPosts, _userSettingsUnset) ? this.imageSearchPosts : imageSearchPosts as List?, - imageSearchComments: identical(imageSearchComments, _userSettingsUnset) ? this.imageSearchComments : imageSearchComments as List?, - ); - } - - @override - List get props => [ - status, - personBlocks, - communityBlocks, - instanceBlocks, - personBeingBlocked, - communityBeingBlocked, - instanceBeingBlocked, - siteResponse, - errorMessage, - errorReason, - images, - imageSearchPosts, - imageSearchComments, - ]; -} diff --git a/lib/src/features/user/presentation/utils/user_session_utils.dart b/lib/src/features/user/presentation/utils/user_session_utils.dart index 407ed0395..4b0a75efb 100644 --- a/lib/src/features/user/presentation/utils/user_session_utils.dart +++ b/lib/src/features/user/presentation/utils/user_session_utils.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/account/account.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/packages/ui/ui.dart' show showThunderDialog; Future showLogOutDialog(BuildContext context) async { @@ -12,10 +13,7 @@ Future showLogOutDialog(BuildContext context) async { bool result = false; await showThunderDialog( context: context, - customBuilder: (alertDialog) => BlocProvider.value( - value: context.read(), - child: alertDialog, - ), + customBuilder: (alertDialog) => BlocProvider.value(value: context.read(), child: alertDialog), title: l10n.confirmLogOutTitle, contentText: l10n.confirmLogOutBody, onSecondaryButtonPressed: (dialogContext) { @@ -25,7 +23,10 @@ Future showLogOutDialog(BuildContext context) async { secondaryButtonText: l10n.cancel, onPrimaryButtonPressed: (dialogContext, _) { result = true; - dialogContext.read().add(RemoveProfile(accountId: dialogContext.read().state.account.id)); + final activeAccount = dialogContext.read().state.activeAccount; + if (activeAccount != null) { + dialogContext.read().add(SessionRemoved(sessionKey: activeAccount.anonymous ? activeAccount.instance : activeAccount.id)); + } Navigator.of(dialogContext).pop(); }, primaryButtonText: l10n.logOut, @@ -37,9 +38,9 @@ Future showLogOutDialog(BuildContext context) async { /// Restores the previous user that was selected in the app, if it has changed. /// Useful to call after invoking a page that may change the currently selected user. void restoreUser(BuildContext context, Account? originalUser) { - final Account newUser = context.read().state.account; + final Account newUser = resolveActiveAccount(context); if (originalUser != null && originalUser.id != newUser.id) { - context.read().add(SwitchProfile(accountId: originalUser.id, reload: false)); + context.read().add(SessionSwitched(sessionKey: originalUser.anonymous ? originalUser.instance : originalUser.id)); } } diff --git a/lib/src/features/user/presentation/widgets/user_action_bottom_sheet.dart b/lib/src/features/user/presentation/widgets/user_action_bottom_sheet.dart index 9ec47b704..52795634c 100644 --- a/lib/src/features/user/presentation/widgets/user_action_bottom_sheet.dart +++ b/lib/src/features/user/presentation/widgets/user_action_bottom_sheet.dart @@ -135,7 +135,11 @@ class _UserActionBottomSheetState extends State { switch (action) { case UserBottomSheetAction.viewProfile: Navigator.of(context).pop(); - navigateToFeedPage(context, feedType: FeedType.user, userId: widget.user.id); + Future.microtask(() { + if (widget.context.mounted) { + navigateToFeedPage(widget.context, account: widget.account, feedType: FeedType.user, userId: widget.user.id); + } + }); break; case UserBottomSheetAction.blockUser: Navigator.of(context).pop(); diff --git a/lib/src/features/user/presentation/widgets/user_header/user_header.dart b/lib/src/features/user/presentation/widgets/user_header/user_header.dart index 7b280d30f..d18409520 100644 --- a/lib/src/features/user/presentation/widgets/user_header/user_header.dart +++ b/lib/src/features/user/presentation/widgets/user_header/user_header.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/feed/feed.dart'; +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/features/user/user.dart'; @@ -56,17 +57,26 @@ class _UserHeaderState extends State { content = GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => showModalBottomSheet( - context: context, - showDragHandle: true, - enableDrag: true, - useSafeArea: true, - scrollControlDisabledMaxHeightRatio: 0.90, - builder: (context) => UserInformation( - user: widget.user, - moderates: widget.moderates, - ), - ), + onTap: () { + final account = resolveEffectiveAccount(context); + + showModalBottomSheet( + context: context, + showDragHandle: true, + enableDrag: true, + useSafeArea: true, + scrollControlDisabledMaxHeightRatio: 0.90, + builder: (_) => wrapWithCapturedAccountContext( + context, + UserInformation( + launchContext: context, + account: account, + user: widget.user, + moderates: widget.moderates, + ), + ), + ); + }, child: _UserHeaderWithBanner(user: widget.user, child: content), ); diff --git a/lib/src/features/user/presentation/widgets/user_information.dart b/lib/src/features/user/presentation/widgets/user_information.dart index 0fb6834bc..408f40bc8 100644 --- a/lib/src/features/user/presentation/widgets/user_information.dart +++ b/lib/src/features/user/presentation/widgets/user_information.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/feed/feed.dart'; @@ -16,6 +17,9 @@ import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; /// A widget that displays detailed information about a user. class UserInformation extends StatefulWidget { + final BuildContext launchContext; + final Account account; + /// The user to display in the sidebar. final ThunderUser user; @@ -24,6 +28,8 @@ class UserInformation extends StatefulWidget { const UserInformation({ super.key, + required this.launchContext, + required this.account, required this.user, required this.moderates, }); @@ -45,7 +51,7 @@ class _UserInformationState extends State { SidebarSectionHeader(value: l10n.profileBio), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: CommonMarkdownBody(body: widget.user.bio ?? '_${l10n.noProfileBioSet}_', imageMaxWidth: MediaQuery.of(context).size.width), + child: CommonMarkdownBody(body: widget.user.bio ?? '_${l10n.noProfileBioSet}_', imageMaxWidth: MediaQuery.of(context).size.width, launchContext: widget.launchContext), ), SidebarSectionHeader(value: l10n.stats), Padding( @@ -61,7 +67,7 @@ class _UserInformationState extends State { SidebarSectionHeader(value: l10n.moderates), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: UserModeratorList(moderates: widget.moderates), + child: UserModeratorList(launchContext: widget.launchContext, account: widget.account, moderates: widget.moderates), ), ], SizedBox(height: MediaQuery.of(context).viewPadding.bottom + 32.0), @@ -157,10 +163,13 @@ class UserActivityList extends StatelessWidget { /// A widget that displays a list of communities moderated by a user. class UserModeratorList extends StatelessWidget { + final BuildContext launchContext; + final Account account; + /// The communities that the user moderates. final List moderates; - const UserModeratorList({super.key, required this.moderates}); + const UserModeratorList({super.key, required this.launchContext, required this.account, required this.moderates}); @override Widget build(BuildContext context) { @@ -168,7 +177,7 @@ class UserModeratorList extends StatelessWidget { children: [ for (final community in moderates) InkWell( - onTap: () => navigateToFeedPage(context, feedType: FeedType.community, communityId: community.id), + onTap: () => navigateToFeedPage(launchContext, account: account, feedType: FeedType.community, communityId: community.id), borderRadius: BorderRadius.circular(50), child: Padding( padding: const EdgeInsets.all(8.0), diff --git a/lib/src/features/user/presentation/widgets/user_selector.dart b/lib/src/features/user/presentation/widgets/user_selector.dart index b06dc8cfc..59ffd74b3 100644 --- a/lib/src/features/user/presentation/widgets/user_selector.dart +++ b/lib/src/features/user/presentation/widgets/user_selector.dart @@ -4,8 +4,7 @@ import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/post/post.dart'; -import 'package:thunder/src/features/search/search.dart'; - +import 'package:thunder/src/features/session/api.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/packages/ui/ui.dart' show showSnackbar; @@ -15,31 +14,8 @@ import 'package:thunder/packages/ui/ui.dart' show showSnackbar; /// This widget provides a method for switching between different user accounts and ensures that the /// target community, post, or comment is federated to the new account's instance before allowing /// the switch. If the content cannot be resolved on the new instance, the switch is blocked. -/// -/// **Usage Examples:** -/// -/// For creating a post in a community: -/// ```dart -/// UserSelector( -/// account: currentAccount, -/// onUserChanged: (account) => handleAccountChange(account), -/// communityActorId: community.actorId, -/// onCommunityChanged: (community) => handleCommunityChange(community), -/// ) -/// ``` -/// -/// For creating a comment on a post: -/// ```dart -/// UserSelector( -/// account: currentAccount, -/// onUserChanged: (account) => handleAccountChange(account), -/// postActorId: post.actorId, -/// onPostChanged: (post) => handlePostChange(post), -/// ) -/// ``` class UserSelector extends StatefulWidget { - /// The currently selected account. - /// This is the account that will be displayed in the selector. + /// The currently selected account. This is the account that will be displayed in the selector. final Account account; /// Callback invoked when the user successfully switches to a different account. @@ -120,6 +96,9 @@ class UserSelector extends StatefulWidget { /// Defaults to `true`. final bool enableAccountSwitching; + /// Optional resolver used to re-resolve route content for the selected account. + final FeatureAccountContentResolver? contentResolver; + const UserSelector({ super.key, required this.account, @@ -131,6 +110,7 @@ class UserSelector extends StatefulWidget { this.parentCommentActorId, this.onParentCommentChanged, this.enableAccountSwitching = true, + this.contentResolver, }); @override @@ -147,20 +127,21 @@ class _UserSelectorState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _loadUserData(widget.account)); + WidgetsBinding.instance.addPostFrameCallback((_) => _load(widget.account)); } @override void didUpdateWidget(UserSelector oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.account.id != widget.account.id) { - _loadUserData(widget.account); + _load(widget.account); } } /// Loads user data for the specified account - Future _loadUserData(Account? account) async { + Future _load(Account? account) async { if (_isLoading) return; + setState(() => _isLoading = true); try { @@ -212,106 +193,59 @@ class _UserSelectorState extends State { final resolvedItems = await _performAccountSwitch(newAccount); if (resolvedItems != null) { - await _loadUserData(newAccount); + await _load(newAccount); _invokeCallbacks(newAccount, resolvedItems); } } /// Performs federation checks and resolves content on the new account's instance - Future?> _performAccountSwitch(Account newAccount) async { + Future _performAccountSwitch(Account newAccount) async { final l10n = GlobalContext.l10n; try { - ThunderCommunity? community; - ThunderPost? post; - ThunderComment? parentComment; - - // Resolve community if needed - if (widget.communityActorId?.isNotEmpty == true) { - community = await _resolveCommunity(newAccount, widget.communityActorId!); - if (community == null) { - showSnackbar(l10n.unableToFindCommunityOnInstance); - return null; - } + final resolvedContent = await (widget.contentResolver ?? FeatureAccountContentResolver()).resolve( + account: newAccount, + request: FeatureAccountResolutionRequest( + communityActorId: widget.communityActorId, + postActorId: widget.postActorId, + parentCommentActorId: widget.parentCommentActorId, + ), + ); + + if (widget.communityActorId?.isNotEmpty == true && resolvedContent.community == null) { + showSnackbar(l10n.unableToFindCommunityOnInstance); + return null; } - // Resolve post if needed - if (widget.postActorId?.isNotEmpty == true) { - post = await _resolvePost(newAccount, widget.postActorId!); - if (post == null) { - showSnackbar(l10n.accountSwitchPostNotFound(newAccount.instance)); - return null; - } + if (widget.postActorId?.isNotEmpty == true && resolvedContent.post == null) { + showSnackbar(l10n.accountSwitchPostNotFound(newAccount.instance)); + return null; } - // Resolve parent comment if needed - if (widget.parentCommentActorId?.isNotEmpty == true) { - parentComment = await _resolveParentComment(newAccount, widget.parentCommentActorId!); - if (parentComment == null) { - showSnackbar(l10n.accountSwitchParentCommentNotFound(newAccount.instance)); - return null; - } + if (widget.parentCommentActorId?.isNotEmpty == true && resolvedContent.parentComment == null) { + showSnackbar(l10n.accountSwitchParentCommentNotFound(newAccount.instance)); + return null; } - return { - 'community': community, - 'post': post, - 'parentComment': parentComment, - }; + return resolvedContent; } catch (e) { showSnackbar(e.toString()); return null; } } - /// Resolves a community on the new account's instance - Future _resolveCommunity(Account account, String actorId) async { - try { - final response = await SearchRepositoryImpl(account: account).resolve(query: actorId); - return response.community; - } catch (e) { - debugPrint('Failed to resolve community: $e'); - return null; - } - } - - /// Resolves a post on the new account's instance - Future _resolvePost(Account account, String actorId) async { - try { - final response = await SearchRepositoryImpl(account: account).resolve(query: actorId); - if (response.post == null) return null; - - final parsedPosts = await parsePosts([response.post!]); - return parsedPosts.isNotEmpty ? parsedPosts.first : null; - } catch (e) { - debugPrint('Failed to resolve post: $e'); - return null; - } - } - - /// Resolves a parent comment on the new account's instance - Future _resolveParentComment(Account account, String actorId) async { - try { - final response = await SearchRepositoryImpl(account: account).resolve(query: actorId); - return response.comment; - } catch (e) { - debugPrint('Failed to resolve parent comment: $e'); - return null; - } - } - /// Invokes the appropriate callbacks after a successful account switch - void _invokeCallbacks(Account newAccount, Map resolvedItems) { + void _invokeCallbacks(Account newAccount, FeatureAccountResolvedContent resolvedItems) { widget.onUserChanged?.call(newAccount); if (widget.communityActorId != null) { - widget.onCommunityChanged?.call(resolvedItems['community']); + widget.onCommunityChanged?.call(resolvedItems.community); } - if (widget.postActorId != null && resolvedItems['post'] != null) { - widget.onPostChanged?.call(resolvedItems['post'] as ThunderPost); + if (widget.postActorId != null && resolvedItems.post != null) { + widget.onPostChanged?.call(resolvedItems.post!); } - if (widget.parentCommentActorId != null && resolvedItems['parentComment'] != null) { - widget.onParentCommentChanged?.call(resolvedItems['parentComment'] as ThunderComment); + if (widget.parentCommentActorId != null && resolvedItems.parentComment != null) { + widget.onParentCommentChanged?.call(resolvedItems.parentComment!); } } @@ -377,29 +311,31 @@ class _UserProfileSelectorState extends State<_UserProfileSelector> { final l10n = GlobalContext.l10n; final theme = Theme.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Text(l10n.account(2), style: theme.textTheme.titleLarge), - ), - _accounts.isEmpty - ? Center(child: Text(l10n.noAccountsAdded)) - : ListView.builder( - shrinkWrap: true, - itemCount: _accounts.length, - itemBuilder: (context, index) { - final account = _accounts[index]; - return ListTile( - title: Text(account.username ?? '-', style: theme.textTheme.titleMedium), - subtitle: Text(account.instance), - onTap: () => Navigator.of(context).pop(account), - ); - }, - ), - ], + return SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Text(l10n.account(2), style: theme.textTheme.titleLarge), + ), + _accounts.isEmpty + ? Center(child: Text(l10n.noAccountsAdded)) + : ListView.builder( + shrinkWrap: true, + itemCount: _accounts.length, + itemBuilder: (context, index) { + final account = _accounts[index]; + return ListTile( + title: Text(account.username ?? '-', style: theme.textTheme.titleMedium), + subtitle: Text(account.instance), + onTap: () => Navigator.of(context).pop(account), + ); + }, + ), + ], + ), ); } } diff --git a/lib/src/features/user/user.dart b/lib/src/features/user/user.dart index 5c7a8c2f0..d0cd6b242 100644 --- a/lib/src/features/user/user.dart +++ b/lib/src/features/user/user.dart @@ -1,6 +1,8 @@ export 'domain/enums/user_action.dart'; export 'package:thunder/src/foundation/primitives/models/thunder_my_user.dart'; -export 'presentation/state/user_settings_bloc.dart'; +export 'presentation/state/account_settings_cubit.dart'; +export 'presentation/state/user_blocks_cubit.dart'; +export 'presentation/state/user_media_cubit.dart'; export 'presentation/pages/media_management_page.dart'; export 'presentation/pages/user_settings_block_page.dart'; export 'presentation/pages/user_settings_page.dart'; diff --git a/lib/src/foundation/contracts/active_account_provider.dart b/lib/src/foundation/contracts/active_account_provider.dart deleted file mode 100644 index eda0a939b..000000000 --- a/lib/src/foundation/contracts/active_account_provider.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:thunder/src/foundation/contracts/account.dart'; - -abstract class ActiveAccountProvider { - Future getActiveAccount(); -} diff --git a/lib/src/foundation/contracts/contracts.dart b/lib/src/foundation/contracts/contracts.dart index 1c5479cd0..76dcae258 100644 --- a/lib/src/foundation/contracts/contracts.dart +++ b/lib/src/foundation/contracts/contracts.dart @@ -1,4 +1,3 @@ -export 'active_account_provider.dart'; export 'account.dart'; export 'connectivity_service.dart'; export 'deep_link_service.dart'; diff --git a/lib/src/foundation/contracts/preferences_store.dart b/lib/src/foundation/contracts/preferences_store.dart index 571c7cee0..8ffc6a1e3 100644 --- a/lib/src/foundation/contracts/preferences_store.dart +++ b/lib/src/foundation/contracts/preferences_store.dart @@ -1,5 +1,5 @@ import 'package:thunder/src/foundation/primitives/enums/local_settings.dart'; -import 'package:thunder/src/foundation/persistence/preferences.dart'; +import 'package:thunder/src/foundation/persistence/preferences/preferences.dart'; abstract class PreferencesStore { T? getLocalSetting(LocalSettings setting); diff --git a/lib/src/foundation/persistence/database/database.dart b/lib/src/foundation/persistence/database/database.dart index fe9ca538e..63c555a3d 100644 --- a/lib/src/foundation/persistence/database/database.dart +++ b/lib/src/foundation/persistence/database/database.dart @@ -11,12 +11,12 @@ import 'package:thunder/src/foundation/primitives/enums/draft_type.dart'; import 'database.steps.dart'; part 'database.g.dart'; -@DriftDatabase(tables: [Accounts, Favorites, LocalSubscriptions, UserLabels, Drafts]) +@DriftDatabase(tables: [Accounts, Favorites, LocalSubscriptions, UserLabels, Drafts, SessionStateTable]) class AppDatabase extends _$AppDatabase { AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); @override - int get schemaVersion => 8; + int get schemaVersion => 9; static QueryExecutor _openConnection() { return driftDatabase( @@ -127,6 +127,9 @@ class AppDatabase extends _$AppDatabase { await customStatement('UPDATE drafts SET nsfw = 0 WHERE nsfw IS NULL'); await customStatement('CREATE UNIQUE INDEX IF NOT EXISTS drafts_single_active_idx ON drafts(active) WHERE active = 1'); }, + from8To9: (m, schema) async { + await m.createTable(schema.sessionState); + }, ), ); @@ -166,7 +169,9 @@ Future _onDowngrade(AppDatabase database, int fromVersion, int toVersion) } Future _onDownGradeOneStep(AppDatabase database, int fromVersion, int toVersion) async { - if (fromVersion == 8 && toVersion == 7) { + if (fromVersion == 9 && toVersion == 8) { + await database.customStatement('DROP TABLE IF EXISTS session_state'); + } else if (fromVersion == 8 && toVersion == 7) { await database.customStatement('DROP INDEX IF EXISTS drafts_single_active_idx'); // Drop active, account_id, nsfw, and language_id columns from drafts diff --git a/lib/src/foundation/persistence/database/database.g.dart b/lib/src/foundation/persistence/database/database.g.dart index 352efa552..b3adf9137 100644 --- a/lib/src/foundation/persistence/database/database.g.dart +++ b/lib/src/foundation/persistence/database/database.g.dart @@ -1487,6 +1487,168 @@ class DraftsCompanion extends UpdateCompanion { } } +class $SessionStateTableTable extends SessionStateTable with TableInfo<$SessionStateTableTable, SessionStateTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SessionStateTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _singletonMeta = const VerificationMeta('singleton'); + @override + late final GeneratedColumn singleton = GeneratedColumn('singleton', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: false, defaultValue: const Constant(0)); + static const VerificationMeta _accountIdMeta = const VerificationMeta('accountId'); + @override + late final GeneratedColumn accountId = GeneratedColumn('account_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES accounts (id) ON DELETE SET NULL')); + @override + List get $columns => [singleton, accountId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'session_state'; + @override + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('singleton')) { + context.handle(_singletonMeta, singleton.isAcceptableOrUnknown(data['singleton']!, _singletonMeta)); + } + if (data.containsKey('account_id')) { + context.handle(_accountIdMeta, accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta)); + } + return context; + } + + @override + Set get $primaryKey => {singleton}; + @override + SessionStateTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SessionStateTableData( + singleton: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}singleton'])!, + accountId: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}account_id']), + ); + } + + @override + $SessionStateTableTable createAlias(String alias) { + return $SessionStateTableTable(attachedDatabase, alias); + } +} + +class SessionStateTableData extends DataClass implements Insertable { + final int singleton; + final int? accountId; + const SessionStateTableData({required this.singleton, this.accountId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['singleton'] = Variable(singleton); + if (!nullToAbsent || accountId != null) { + map['account_id'] = Variable(accountId); + } + return map; + } + + SessionStateTableCompanion toCompanion(bool nullToAbsent) { + return SessionStateTableCompanion( + singleton: Value(singleton), + accountId: accountId == null && nullToAbsent ? const Value.absent() : Value(accountId), + ); + } + + factory SessionStateTableData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SessionStateTableData( + singleton: serializer.fromJson(json['singleton']), + accountId: serializer.fromJson(json['accountId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'singleton': serializer.toJson(singleton), + 'accountId': serializer.toJson(accountId), + }; + } + + SessionStateTableData copyWith({int? singleton, Value accountId = const Value.absent()}) => SessionStateTableData( + singleton: singleton ?? this.singleton, + accountId: accountId.present ? accountId.value : this.accountId, + ); + SessionStateTableData copyWithCompanion(SessionStateTableCompanion data) { + return SessionStateTableData( + singleton: data.singleton.present ? data.singleton.value : this.singleton, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + ); + } + + @override + String toString() { + return (StringBuffer('SessionStateTableData(') + ..write('singleton: $singleton, ') + ..write('accountId: $accountId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(singleton, accountId); + @override + bool operator ==(Object other) => identical(this, other) || (other is SessionStateTableData && other.singleton == this.singleton && other.accountId == this.accountId); +} + +class SessionStateTableCompanion extends UpdateCompanion { + final Value singleton; + final Value accountId; + const SessionStateTableCompanion({ + this.singleton = const Value.absent(), + this.accountId = const Value.absent(), + }); + SessionStateTableCompanion.insert({ + this.singleton = const Value.absent(), + this.accountId = const Value.absent(), + }); + static Insertable custom({ + Expression? singleton, + Expression? accountId, + }) { + return RawValuesInsertable({ + if (singleton != null) 'singleton': singleton, + if (accountId != null) 'account_id': accountId, + }); + } + + SessionStateTableCompanion copyWith({Value? singleton, Value? accountId}) { + return SessionStateTableCompanion( + singleton: singleton ?? this.singleton, + accountId: accountId ?? this.accountId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (singleton.present) { + map['singleton'] = Variable(singleton.value); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SessionStateTableCompanion(') + ..write('singleton: $singleton, ') + ..write('accountId: $accountId') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); $AppDatabaseManager get managers => $AppDatabaseManager(this); @@ -1495,10 +1657,22 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $LocalSubscriptionsTable localSubscriptions = $LocalSubscriptionsTable(this); late final $UserLabelsTable userLabels = $UserLabelsTable(this); late final $DraftsTable drafts = $DraftsTable(this); + late final $SessionStateTableTable sessionStateTable = $SessionStateTableTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [accounts, favorites, localSubscriptions, userLabels, drafts]; + List get allSchemaEntities => [accounts, favorites, localSubscriptions, userLabels, drafts, sessionStateTable]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( + [ + WritePropagation( + on: TableUpdateQuery.onTableName('accounts', limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('session_state', kind: UpdateKind.update), + ], + ), + ], + ); } typedef $$AccountsTableCreateCompanionBuilder = AccountsCompanion Function({ @@ -1522,6 +1696,20 @@ typedef $$AccountsTableUpdateCompanionBuilder = AccountsCompanion Function({ Value platform, }); +final class $$AccountsTableReferences extends BaseReferences<_$AppDatabase, $AccountsTable, Account> { + $$AccountsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$SessionStateTableTable, List> _sessionStateTableRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable(db.sessionStateTable, aliasName: $_aliasNameGenerator(db.accounts.id, db.sessionStateTable.accountId)); + + $$SessionStateTableTableProcessedTableManager get sessionStateTableRefs { + final manager = $$SessionStateTableTableTableManager($_db, $_db.sessionStateTable).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_sessionStateTableRefsTable($_db)); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: cache)); + } +} + class $$AccountsTableFilterComposer extends Composer<_$AppDatabase, $AccountsTable> { $$AccountsTableFilterComposer({ required super.$db, @@ -1546,6 +1734,22 @@ class $$AccountsTableFilterComposer extends Composer<_$AppDatabase, $AccountsTab ColumnWithTypeConverterFilters get platform => $composableBuilder(column: $table.platform, builder: (column) => ColumnWithTypeConverterFilters(column)); + + Expression sessionStateTableRefs(Expression Function($$SessionStateTableTableFilterComposer f) f) { + final $$SessionStateTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.sessionStateTable, + getReferencedColumn: (t) => t.accountId, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => $$SessionStateTableTableFilterComposer( + $db: $db, + $table: $db.sessionStateTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$AccountsTableOrderingComposer extends Composer<_$AppDatabase, $AccountsTable> { @@ -1596,10 +1800,26 @@ class $$AccountsTableAnnotationComposer extends Composer<_$AppDatabase, $Account GeneratedColumn get listIndex => $composableBuilder(column: $table.listIndex, builder: (column) => column); GeneratedColumnWithTypeConverter get platform => $composableBuilder(column: $table.platform, builder: (column) => column); + + Expression sessionStateTableRefs(Expression Function($$SessionStateTableTableAnnotationComposer a) f) { + final $$SessionStateTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.sessionStateTable, + getReferencedColumn: (t) => t.accountId, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => $$SessionStateTableTableAnnotationComposer( + $db: $db, + $table: $db.sessionStateTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } class $$AccountsTableTableManager extends RootTableManager<_$AppDatabase, $AccountsTable, Account, $$AccountsTableFilterComposer, $$AccountsTableOrderingComposer, $$AccountsTableAnnotationComposer, - $$AccountsTableCreateCompanionBuilder, $$AccountsTableUpdateCompanionBuilder, (Account, BaseReferences<_$AppDatabase, $AccountsTable, Account>), Account, PrefetchHooks Function()> { + $$AccountsTableCreateCompanionBuilder, $$AccountsTableUpdateCompanionBuilder, (Account, $$AccountsTableReferences), Account, PrefetchHooks Function({bool sessionStateTableRefs})> { $$AccountsTableTableManager(_$AppDatabase db, $AccountsTable table) : super(TableManagerState( db: db, @@ -1647,8 +1867,25 @@ class $$AccountsTableTableManager extends RootTableManager<_$AppDatabase, $Accou listIndex: listIndex, platform: platform, ), - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), - prefetchHooksCallback: null, + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), $$AccountsTableReferences(db, table, e))).toList(), + prefetchHooksCallback: ({sessionStateTableRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (sessionStateTableRefs) db.sessionStateTable], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (sessionStateTableRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$AccountsTableReferences._sessionStateTableRefsTable(db), + managerFromTypedResult: (p0) => $$AccountsTableReferences(db, table, p0).sessionStateTableRefs, + referencedItemsForCurrentItem: (item, referencedItems) => referencedItems.where((e) => e.accountId == item.id), + typedResults: items) + ]; + }, + ); + }, )); } @@ -1661,9 +1898,9 @@ typedef $$AccountsTableProcessedTableManager = ProcessedTableManager< $$AccountsTableAnnotationComposer, $$AccountsTableCreateCompanionBuilder, $$AccountsTableUpdateCompanionBuilder, - (Account, BaseReferences<_$AppDatabase, $AccountsTable, Account>), + (Account, $$AccountsTableReferences), Account, - PrefetchHooks Function()>; + PrefetchHooks Function({bool sessionStateTableRefs})>; typedef $$FavoritesTableCreateCompanionBuilder = FavoritesCompanion Function({ Value id, required int accountId, @@ -2236,6 +2473,183 @@ class $$DraftsTableTableManager extends RootTableManager<_$AppDatabase, $DraftsT typedef $$DraftsTableProcessedTableManager = ProcessedTableManager<_$AppDatabase, $DraftsTable, Draft, $$DraftsTableFilterComposer, $$DraftsTableOrderingComposer, $$DraftsTableAnnotationComposer, $$DraftsTableCreateCompanionBuilder, $$DraftsTableUpdateCompanionBuilder, (Draft, BaseReferences<_$AppDatabase, $DraftsTable, Draft>), Draft, PrefetchHooks Function()>; +typedef $$SessionStateTableTableCreateCompanionBuilder = SessionStateTableCompanion Function({ + Value singleton, + Value accountId, +}); +typedef $$SessionStateTableTableUpdateCompanionBuilder = SessionStateTableCompanion Function({ + Value singleton, + Value accountId, +}); + +final class $$SessionStateTableTableReferences extends BaseReferences<_$AppDatabase, $SessionStateTableTable, SessionStateTableData> { + $$SessionStateTableTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $AccountsTable _accountIdTable(_$AppDatabase db) => db.accounts.createAlias($_aliasNameGenerator(db.sessionStateTable.accountId, db.accounts.id)); + + $$AccountsTableProcessedTableManager? get accountId { + final $_column = $_itemColumn('account_id'); + if ($_column == null) return null; + final manager = $$AccountsTableTableManager($_db, $_db.accounts).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_accountIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$SessionStateTableTableFilterComposer extends Composer<_$AppDatabase, $SessionStateTableTable> { + $$SessionStateTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get singleton => $composableBuilder(column: $table.singleton, builder: (column) => ColumnFilters(column)); + + $$AccountsTableFilterComposer get accountId { + final $$AccountsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => $$AccountsTableFilterComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SessionStateTableTableOrderingComposer extends Composer<_$AppDatabase, $SessionStateTableTable> { + $$SessionStateTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get singleton => $composableBuilder(column: $table.singleton, builder: (column) => ColumnOrderings(column)); + + $$AccountsTableOrderingComposer get accountId { + final $$AccountsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => $$AccountsTableOrderingComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SessionStateTableTableAnnotationComposer extends Composer<_$AppDatabase, $SessionStateTableTable> { + $$SessionStateTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get singleton => $composableBuilder(column: $table.singleton, builder: (column) => column); + + $$AccountsTableAnnotationComposer get accountId { + final $$AccountsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => $$AccountsTableAnnotationComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SessionStateTableTableTableManager extends RootTableManager< + _$AppDatabase, + $SessionStateTableTable, + SessionStateTableData, + $$SessionStateTableTableFilterComposer, + $$SessionStateTableTableOrderingComposer, + $$SessionStateTableTableAnnotationComposer, + $$SessionStateTableTableCreateCompanionBuilder, + $$SessionStateTableTableUpdateCompanionBuilder, + (SessionStateTableData, $$SessionStateTableTableReferences), + SessionStateTableData, + PrefetchHooks Function({bool accountId})> { + $$SessionStateTableTableTableManager(_$AppDatabase db, $SessionStateTableTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => $$SessionStateTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$SessionStateTableTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$SessionStateTableTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value singleton = const Value.absent(), + Value accountId = const Value.absent(), + }) => + SessionStateTableCompanion( + singleton: singleton, + accountId: accountId, + ), + createCompanionCallback: ({ + Value singleton = const Value.absent(), + Value accountId = const Value.absent(), + }) => + SessionStateTableCompanion.insert( + singleton: singleton, + accountId: accountId, + ), + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), $$SessionStateTableTableReferences(db, table, e))).toList(), + prefetchHooksCallback: ({accountId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: >(state) { + if (accountId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.accountId, + referencedTable: $$SessionStateTableTableReferences._accountIdTable(db), + referencedColumn: $$SessionStateTableTableReferences._accountIdTable(db).id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$SessionStateTableTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $SessionStateTableTable, + SessionStateTableData, + $$SessionStateTableTableFilterComposer, + $$SessionStateTableTableOrderingComposer, + $$SessionStateTableTableAnnotationComposer, + $$SessionStateTableTableCreateCompanionBuilder, + $$SessionStateTableTableUpdateCompanionBuilder, + (SessionStateTableData, $$SessionStateTableTableReferences), + SessionStateTableData, + PrefetchHooks Function({bool accountId})>; class $AppDatabaseManager { final _$AppDatabase _db; @@ -2245,4 +2659,5 @@ class $AppDatabaseManager { $$LocalSubscriptionsTableTableManager get localSubscriptions => $$LocalSubscriptionsTableTableManager(_db, _db.localSubscriptions); $$UserLabelsTableTableManager get userLabels => $$UserLabelsTableTableManager(_db, _db.userLabels); $$DraftsTableTableManager get drafts => $$DraftsTableTableManager(_db, _db.drafts); + $$SessionStateTableTableTableManager get sessionStateTable => $$SessionStateTableTableTableManager(_db, _db.sessionStateTable); } diff --git a/lib/src/foundation/persistence/database/database.steps.dart b/lib/src/foundation/persistence/database/database.steps.dart index 64d4d9437..e8ac1f5a8 100644 --- a/lib/src/foundation/persistence/database/database.steps.dart +++ b/lib/src/foundation/persistence/database/database.steps.dart @@ -831,6 +831,132 @@ i1.GeneratedColumn _column_49(String aliasedName) => i1.GeneratedColumn('nsfw', aliasedName, false, type: i1.DriftSqlType.int, $customConstraints: 'NOT NULL DEFAULT 0 CHECK (nsfw IN (0, 1))', defaultValue: const i1.CustomExpression('0')); i1.GeneratedColumn _column_50(String aliasedName) => i1.GeneratedColumn('language_id', aliasedName, true, type: i1.DriftSqlType.int, $customConstraints: 'NULL'); i1.GeneratedColumn _column_51(String aliasedName) => i1.GeneratedColumn('body', aliasedName, true, type: i1.DriftSqlType.string, $customConstraints: 'NULL'); + +final class Schema9 extends i0.VersionedSchema { + Schema9({required super.database}) : super(version: 9); + @override + late final List entities = [ + accounts, + favorites, + localSubscriptions, + userLabels, + drafts, + sessionState, + ]; + late final Shape9 accounts = Shape9( + source: i0.VersionedTable( + entityName: 'accounts', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_24, + _column_25, + _column_26, + _column_27, + _column_28, + _column_29, + _column_30, + _column_31, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 favorites = Shape1( + source: i0.VersionedTable( + entityName: 'favorites', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_24, + _column_32, + _column_33, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape2 localSubscriptions = Shape2( + source: i0.VersionedTable( + entityName: 'local_subscriptions', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_24, + _column_34, + _column_35, + _column_36, + _column_37, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape3 userLabels = Shape3( + source: i0.VersionedTable( + entityName: 'user_labels', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_24, + _column_38, + _column_39, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 drafts = Shape10( + source: i0.VersionedTable( + entityName: 'drafts', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_24, + _column_40, + _column_41, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape11 sessionState = Shape11( + source: i0.VersionedTable( + entityName: 'session_state', + withoutRowId: false, + isStrict: false, + tableConstraints: [ + 'PRIMARY KEY(singleton)', + ], + columns: [ + _column_52, + _column_53, + ], + attachedDatabase: database, + ), + alias: null); +} + +class Shape11 extends i0.VersionedTable { + Shape11({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get singleton => columnsByName['singleton']! as i1.GeneratedColumn; + i1.GeneratedColumn get accountId => columnsByName['account_id']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_52(String aliasedName) => + i1.GeneratedColumn('singleton', aliasedName, false, type: i1.DriftSqlType.int, $customConstraints: 'NOT NULL DEFAULT 0', defaultValue: const i1.CustomExpression('0')); +i1.GeneratedColumn _column_53(String aliasedName) => + i1.GeneratedColumn('account_id', aliasedName, true, type: i1.DriftSqlType.int, $customConstraints: 'NULL REFERENCES accounts(id)ON DELETE SET NULL'); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -839,6 +965,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema6 schema) from5To6, required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, + required Future Function(i1.Migrator m, Schema9 schema) from8To9, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -877,6 +1004,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from7To8(migrator, schema); return 8; + case 8: + final schema = Schema9(database: database); + final migrator = i1.Migrator(database, schema); + await from8To9(migrator, schema); + return 9; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -891,6 +1023,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema6 schema) from5To6, required Future Function(i1.Migrator m, Schema7 schema) from6To7, required Future Function(i1.Migrator m, Schema8 schema) from7To8, + required Future Function(i1.Migrator m, Schema9 schema) from8To9, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( @@ -901,4 +1034,5 @@ i1.OnUpgrade stepByStep({ from5To6: from5To6, from6To7: from6To7, from7To8: from7To8, + from8To9: from8To9, )); diff --git a/lib/src/foundation/persistence/database_provider.dart b/lib/src/foundation/persistence/database/initialization.dart similarity index 60% rename from lib/src/foundation/persistence/database_provider.dart rename to lib/src/foundation/persistence/database/initialization.dart index dc96950fa..4136c375a 100644 --- a/lib/src/foundation/persistence/database_provider.dart +++ b/lib/src/foundation/persistence/database/initialization.dart @@ -2,6 +2,7 @@ import 'package:thunder/src/foundation/persistence/database/database.dart'; late AppDatabase database; +/// Initializes the database instance. This should be called before any database operations are performed. void initializeDatabase() { database = AppDatabase(); } diff --git a/lib/src/foundation/persistence/database/schemas/thunder/drift_schema_v9.json b/lib/src/foundation/persistence/database/schemas/thunder/drift_schema_v9.json new file mode 100644 index 000000000..ca2a630e3 --- /dev/null +++ b/lib/src/foundation/persistence/database/schemas/thunder/drift_schema_v9.json @@ -0,0 +1,562 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "accounts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "username", + "getter_name": "username", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "jwt", + "getter_name": "jwt", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "instance", + "getter_name": "instance", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "anonymous", + "getter_name": "anonymous", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"anonymous\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"anonymous\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_id", + "getter_name": "userId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "list_index", + "getter_name": "listIndex", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('-1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "platform", + "getter_name": "platform", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const ThreadiversePlatformConverter()", + "dart_type_name": "ThreadiversePlatform?" + } + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "favorites", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "account_id", + "getter_name": "accountId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "community_id", + "getter_name": "communityId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 2, + "references": [], + "type": "table", + "data": { + "name": "local_subscriptions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "title", + "getter_name": "title", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "actor_id", + "getter_name": "actorId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "icon", + "getter_name": "icon", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 3, + "references": [], + "type": "table", + "data": { + "name": "user_labels", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "username", + "getter_name": "username", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "label", + "getter_name": "label", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 4, + "references": [], + "type": "table", + "data": { + "name": "drafts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "draft_type", + "getter_name": "draftType", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const DraftTypeConverter()", + "dart_type_name": "DraftType" + } + }, + { + "name": "existing_id", + "getter_name": "existingId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "reply_id", + "getter_name": "replyId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "active", + "getter_name": "active", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"active\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"active\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "account_id", + "getter_name": "accountId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "title", + "getter_name": "title", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "url", + "getter_name": "url", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "custom_thumbnail", + "getter_name": "customThumbnail", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "alt_text", + "getter_name": "altText", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "nsfw", + "getter_name": "nsfw", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"nsfw\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"nsfw\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "language_id", + "getter_name": "languageId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "body", + "getter_name": "body", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 5, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "session_state", + "was_declared_in_moor": false, + "columns": [ + { + "name": "singleton", + "getter_name": "singleton", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "account_id", + "getter_name": "accountId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES accounts (id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES accounts (id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "accounts", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "singleton" + ] + } + } + ], + "fixed_sql": [ + { + "name": "accounts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"accounts\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"username\" TEXT NULL, \"jwt\" TEXT NULL, \"instance\" TEXT NULL, \"anonymous\" INTEGER NOT NULL DEFAULT 0 CHECK (\"anonymous\" IN (0, 1)), \"user_id\" INTEGER NULL, \"list_index\" INTEGER NOT NULL DEFAULT -1, \"platform\" TEXT NULL);" + } + ] + }, + { + "name": "favorites", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"favorites\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"account_id\" INTEGER NOT NULL, \"community_id\" INTEGER NOT NULL);" + } + ] + }, + { + "name": "local_subscriptions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"local_subscriptions\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"name\" TEXT NOT NULL, \"title\" TEXT NOT NULL, \"actor_id\" TEXT NOT NULL, \"icon\" TEXT NULL);" + } + ] + }, + { + "name": "user_labels", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_labels\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"username\" TEXT NOT NULL, \"label\" TEXT NOT NULL);" + } + ] + }, + { + "name": "drafts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"drafts\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"draft_type\" TEXT NOT NULL, \"existing_id\" INTEGER NULL, \"reply_id\" INTEGER NULL, \"active\" INTEGER NOT NULL DEFAULT 0 CHECK (\"active\" IN (0, 1)), \"account_id\" TEXT NULL, \"title\" TEXT NULL, \"url\" TEXT NULL, \"custom_thumbnail\" TEXT NULL, \"alt_text\" TEXT NULL, \"nsfw\" INTEGER NOT NULL DEFAULT 0 CHECK (\"nsfw\" IN (0, 1)), \"language_id\" INTEGER NULL, \"body\" TEXT NULL);" + } + ] + }, + { + "name": "session_state", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"session_state\" (\"singleton\" INTEGER NOT NULL DEFAULT 0, \"account_id\" INTEGER NULL REFERENCES accounts (id) ON DELETE SET NULL, PRIMARY KEY (\"singleton\"));" + } + ] + } + ] +} \ No newline at end of file diff --git a/lib/src/foundation/persistence/database/tables.dart b/lib/src/foundation/persistence/database/tables.dart index 6ab0b77de..be1f3e1c1 100644 --- a/lib/src/foundation/persistence/database/tables.dart +++ b/lib/src/foundation/persistence/database/tables.dart @@ -48,3 +48,14 @@ class Drafts extends Table { IntColumn get languageId => integer().nullable()(); TextColumn get body => text().nullable()(); } + +class SessionStateTable extends Table { + @override + String get tableName => 'session_state'; + + IntColumn get singleton => integer().withDefault(const Constant(0))(); + IntColumn get accountId => integer().nullable().references(Accounts, #id, onDelete: KeyAction.setNull)(); + + @override + Set get primaryKey => {singleton}; +} diff --git a/lib/src/foundation/persistence/persistence.dart b/lib/src/foundation/persistence/persistence.dart index 11934f810..e041f698e 100644 --- a/lib/src/foundation/persistence/persistence.dart +++ b/lib/src/foundation/persistence/persistence.dart @@ -2,5 +2,6 @@ export 'database/database.dart' hide Account, Favorite, LocalSubscription, UserL export 'database/database_utils.dart'; export 'database/tables.dart'; export 'database/type_converters.dart'; -export 'preferences.dart'; -export 'database_provider.dart'; +export 'preferences/preferences.dart'; +export 'preferences/preferences_migration.dart'; +export 'database/initialization.dart'; diff --git a/lib/src/foundation/persistence/preferences.dart b/lib/src/foundation/persistence/preferences/preferences.dart similarity index 100% rename from lib/src/foundation/persistence/preferences.dart rename to lib/src/foundation/persistence/preferences/preferences.dart diff --git a/lib/src/app/bootstrap/preferences_migration.dart b/lib/src/foundation/persistence/preferences/preferences_migration.dart similarity index 100% rename from lib/src/app/bootstrap/preferences_migration.dart rename to lib/src/foundation/persistence/preferences/preferences_migration.dart diff --git a/lib/src/foundation/primitives/enums/local_settings.dart b/lib/src/foundation/primitives/enums/local_settings.dart index f9237be7f..7cd8e671c 100644 --- a/lib/src/foundation/primitives/enums/local_settings.dart +++ b/lib/src/foundation/primitives/enums/local_settings.dart @@ -388,8 +388,6 @@ enum LocalSettings { name: 'setting_enable_experimental_features', key: 'enableExperimentalFeatures', category: LocalSettingsCategories.debug, subCategory: LocalSettingsSubCategories.experimental), imageDimensionTimeout(name: 'setting_image_dimension_timeout', key: 'imageDimensionTimeout', category: LocalSettingsCategories.debug, subCategory: LocalSettingsSubCategories.feed), - currentAnonymousInstance(name: 'setting_current_anonymous_instance', key: '', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.general, searchable: false), - // This setting exists purely to save/load the user's selected advanced share options advancedShareOptions(name: 'advanced_share_options', key: '', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.general, searchable: false), // import export settings @@ -474,7 +472,6 @@ enum LocalSettings { /// Defines the settings that are excluded from import/export static List importExportExcludedSettings = [ - LocalSettings.currentAnonymousInstance, LocalSettings.advancedShareOptions, ]; } diff --git a/lib/src/shared/content/utils/media/media_utils.dart b/lib/src/shared/content/utils/media/media_utils.dart index 44a3e7536..fc50dea99 100644 --- a/lib/src/shared/content/utils/media/media_utils.dart +++ b/lib/src/shared/content/utils/media/media_utils.dart @@ -19,6 +19,7 @@ import 'package:image_dimension_parser/image_dimension_parser.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; +import 'package:thunder/src/shared/content/widgets/media/experimental_image_viewer.dart'; import 'package:thunder/src/shared/content/widgets/media/image_viewer.dart'; final Map _imageDimensionsCache = {}; @@ -237,8 +238,43 @@ Future> selectImagesToUpload({bool allowMultiple = false}) async { return [file.path]; } +bool useExperimentalImageViewer(BuildContext context) { + return context.read().state.enableExperimentalFeatures; +} + +Widget buildImageViewerWidget( + BuildContext context, { + String? altText, + Uint8List? bytes, + bool? clearMemoryCacheWhenDispose, + bool isPeek = false, + void Function()? navigateToPost, + int? postId, + String? url, +}) { + if (useExperimentalImageViewer(context)) { + return ExperimentalImageViewer( + altText: altText, + bytes: bytes, + isPeek: isPeek, + navigateToPost: navigateToPost, + url: url, + ); + } + + return ImageViewer( + url: url, + bytes: bytes, + postId: postId, + navigateToPost: navigateToPost, + altText: altText, + isPeek: isPeek, + clearMemoryCacheWhenDispose: clearMemoryCacheWhenDispose ?? false, + ); +} + void showImageViewer(BuildContext context, {String? url, Uint8List? bytes, int? postId, void Function()? navigateToPost, String? altText, bool? clearMemoryCacheWhenDispose}) { - final resolvedClearMemoryCacheWhenDispose = clearMemoryCacheWhenDispose ?? context.read().state.imageCachingMode == ImageCachingMode.relaxed; + final resolvedClearMemoryCacheWhenDispose = clearMemoryCacheWhenDispose ?? context.read().state.imageCachingMode == ImageCachingMode.relaxed; Navigator.of(context).push( PageRouteBuilder( @@ -246,7 +282,8 @@ void showImageViewer(BuildContext context, {String? url, Uint8List? bytes, int? transitionDuration: const Duration(milliseconds: 100), reverseTransitionDuration: const Duration(milliseconds: 50), pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { - return ImageViewer( + return buildImageViewerWidget( + context, url: url, bytes: bytes, postId: postId, diff --git a/lib/src/shared/content/widgets/markdown/common_markdown_body.dart b/lib/src/shared/content/widgets/markdown/common_markdown_body.dart index f7100c4ab..63ad053d6 100644 --- a/lib/src/shared/content/widgets/markdown/common_markdown_body.dart +++ b/lib/src/shared/content/widgets/markdown/common_markdown_body.dart @@ -38,6 +38,9 @@ class CommonMarkdownBody extends StatefulWidget { /// The maximum width of the image. final double? imageMaxWidth; + /// Optional source context used for link handling when this markdown is shown in an overlay. + final BuildContext? launchContext; + const CommonMarkdownBody({ super.key, required this.body, @@ -45,6 +48,7 @@ class CommonMarkdownBody extends StatefulWidget { this.nsfw = false, this.isComment, this.imageMaxWidth, + this.launchContext, }); @override @@ -113,6 +117,8 @@ class _CommonMarkdownBodyState extends State { final accessibilityOn = SemanticsBinding.instance.accessibilityFeatures.accessibleNavigation; + final navigationContext = widget.launchContext ?? context; + return ExcludeSemantics( excluding: !accessibilityOn, child: RepaintBoundary( @@ -131,10 +137,10 @@ class _CommonMarkdownBodyState extends State { imageMaxWidth: widget.imageMaxWidth, ), onTapLink: (text, url, title) { - if (url != null) handleLink(context, url: url); + if (url != null) handleLink(navigationContext, url: url); }, onLongPressLink: (text, url, title) { - if (url != null) handleLinkLongPress(context, text, url); + if (url != null) handleLinkLongPress(navigationContext, text, url); }, styleSheet: styleSheet.copyWith(textScaleFactor: _getTextScaleFactor()), ), diff --git a/lib/src/shared/content/widgets/media/experimental_image_viewer.dart b/lib/src/shared/content/widgets/media/experimental_image_viewer.dart new file mode 100644 index 000000000..568e4a67b --- /dev/null +++ b/lib/src/shared/content/widgets/media/experimental_image_viewer.dart @@ -0,0 +1,499 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:gal/gal.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:thunder/l10n/generated/app_localizations.dart'; +import 'package:thunder/packages/ui/ui.dart' show ThunderImageViewer, ThunderImageViewerSource, showSnackbar; +import 'package:thunder/src/shared/content/utils/media/media_utils.dart'; + +/// An experimental Thunder-specific image viewer built on top of +/// [ThunderImageViewer]. +/// +/// This widget adds application-level behavior that should not live in the UI +/// package, such as system UI coordination, media actions, post navigation, and +/// the alt-text overlay. +/// +/// The underlying gesture implementation is fully provided by +/// [ThunderImageViewer]. +class ExperimentalImageViewer extends StatefulWidget { + /// Creates an experimental image viewer. + /// + /// Either [url] or [bytes] must be provided. + const ExperimentalImageViewer({ + super.key, + this.altText, + this.bytes, + this.isPeek = false, + this.navigateToPost, + this.url, + }) : assert(url != null || bytes != null); + + /// The alternative text displayed by the viewer, if available. + final String? altText; + + /// The encoded image bytes to display. + /// + /// This is used when the viewer is opened from an in-memory image instead of + /// a network URL. + final Uint8List? bytes; + + /// Whether the viewer is being shown as a lightweight peek overlay. + /// + /// Peek mode suppresses the full viewer chrome and avoids system UI changes. + final bool isPeek; + + /// Called when the user chooses to navigate to the originating post. + final VoidCallback? navigateToPost; + + /// The URL of the image to display. + final String? url; + + @override + State createState() => _ExperimentalImageViewerState(); +} + +class _ExperimentalImageViewerState extends State { + static const double _fullscreenScaleThreshold = 1.2; + + bool _autoFullscreen = false; + bool _downloaded = false; + bool _isChromeVisible = true; + bool _isDownloadingMedia = false; + bool _isSavingMedia = false; + bool _showAltText = false; + + double _maxScale = 4.0; + + Size? _imageSize; + + ThunderImageViewerSource get _source { + if (widget.url != null) { + return ThunderImageViewerSource.network(widget.url!); + } + + return ThunderImageViewerSource.memory(widget.bytes!); + } + + @override + void initState() { + super.initState(); + _isChromeVisible = !widget.isPeek; + + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadImageSize(); + _syncSystemUiMode(); + }); + } + + @override + void dispose() { + _restoreSystemUi(); + super.dispose(); + } + + Future _loadImageSize() async { + try { + final decodedImage = await retrieveImageDimensions(imageUrl: widget.url, imageBytes: widget.bytes).timeout(const Duration(seconds: 2)); + + if (!mounted) return; + + setState(() { + _imageSize = decodedImage; + _maxScale = max(decodedImage.width, decodedImage.height) / 128; + if (_maxScale < 3) _maxScale = 3; + }); + } catch (_) { + if (!mounted) return; + setState(() => _maxScale = 4.0); + } + } + + void _syncSystemUiMode() { + if (widget.isPeek) return; + + if (_isChromeVisible) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: SystemUiOverlay.values); + } else { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + } + } + + void _restoreSystemUi() { + if (widget.isPeek) return; + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: SystemUiOverlay.values); + } + + void _setChromeVisible(bool value) { + if (_isChromeVisible == value) return; + + setState(() => _isChromeVisible = value); + _syncSystemUiMode(); + } + + void _enterFullscreen({bool auto = false}) { + if (widget.isPeek) return; + + _autoFullscreen = auto; + _setChromeVisible(false); + } + + void _exitFullscreen() { + if (widget.isPeek) return; + + _autoFullscreen = false; + _setChromeVisible(true); + } + + void _toggleFullscreen() { + if (_isChromeVisible) { + _enterFullscreen(); + } else { + _exitFullscreen(); + } + } + + void _handleViewerLongPress() { + if (widget.isPeek) return; + + HapticFeedback.lightImpact(); + _toggleFullscreen(); + } + + void _handleViewerScaleChanged(double scale) { + if (widget.isPeek) return; + + if (scale > _fullscreenScaleThreshold) { + _enterFullscreen(auto: true); + return; + } + + if (_autoFullscreen) { + _exitFullscreen(); + } + } + + void _handleViewerTap() { + if (widget.isPeek) return; + + if (_isChromeVisible) { + _closeViewer(); + } else { + _exitFullscreen(); + } + } + + void _closeViewer() { + _restoreSystemUi(); + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + } + + Future _resolveMediaFile() async { + final url = widget.url; + + if (url == null) { + final bytes = widget.bytes!; + final directory = await getTemporaryDirectory(); + final file = File(path.join(directory.path, 'thunder-image-${DateTime.now().millisecondsSinceEpoch}.png')); + return file.writeAsBytes(bytes, flush: true); + } + + final cachedFile = await DefaultCacheManager().getFileFromCache(url); + if (cachedFile != null) return cachedFile.file; + + return DefaultCacheManager().getSingleFile(url); + } + + Future _shareImage() async { + final l10n = AppLocalizations.of(context)!; + + try { + setState(() => _isDownloadingMedia = true); + final file = await _resolveMediaFile(); + + await SharePlus.instance.share(ShareParams( + files: [XFile(file.path)], + sharePositionOrigin: const Rect.fromLTWH(0, 0, 1, 1), + )); + } catch (e) { + showSnackbar(l10n.errorDownloadingMedia(e)); + } finally { + if (mounted) { + setState(() => _isDownloadingMedia = false); + } + } + } + + Future _saveImage() async { + final l10n = AppLocalizations.of(context)!; + + try { + setState(() => _isSavingMedia = true); + final file = await _resolveMediaFile(); + + if (!kIsWeb) { + final hasPermission = await Gal.hasAccess(toAlbum: true); + if (!hasPermission) { + await Gal.requestAccess(toAlbum: true); + } + } + + if (!kIsWeb && Platform.isLinux) { + final filePath = '${(await getApplicationDocumentsDirectory()).path}/Thunder/${path.basename(file.path)}'; + + File(filePath) + ..createSync(recursive: true) + ..writeAsBytesSync(file.readAsBytesSync()); + + if (!mounted) return; + setState(() => _downloaded = true); + return; + } + + await Gal.putImage(file.path, album: 'Thunder'); + + if (!mounted) return; + setState(() => _downloaded = true); + } on GalException catch (e) { + if (mounted) { + showSnackbar(e.type.message); + setState(() => _downloaded = false); + } + } catch (e) { + if (mounted) { + showSnackbar(l10n.errorDownloadingMedia(e)); + } + } finally { + if (mounted) { + setState(() => _isSavingMedia = false); + } + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final overlayStyle = const SystemUiOverlayStyle( + statusBarBrightness: Brightness.dark, + statusBarIconBrightness: Brightness.light, + ); + + return PopScope( + onPopInvokedWithResult: (didPop, result) { + if (didPop) { + _restoreSystemUi(); + } + }, + child: AnnotatedRegion( + value: overlayStyle, + child: Material( + color: Colors.black, + child: Stack( + children: [ + Positioned.fill( + child: ThunderImageViewer( + backgroundColor: Colors.black, + contentSize: _imageSize, + dismissible: !widget.isPeek, + maxScale: _maxScale, + onLongPress: widget.isPeek ? null : _handleViewerLongPress, + onDismiss: widget.isPeek ? null : _closeViewer, + onScaleChanged: widget.isPeek ? null : _handleViewerScaleChanged, + onTap: widget.isPeek ? null : _handleViewerTap, + semanticLabel: widget.altText, + source: _source, + ), + ), + if (!widget.isPeek) ...[ + Positioned( + left: 0, + right: 0, + top: 0, + child: SafeArea( + bottom: false, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 180), + opacity: _isChromeVisible ? 1 : 0, + child: IgnorePointer( + ignoring: !_isChromeVisible, + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black54, Colors.transparent], + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Row( + children: [ + IconButton( + onPressed: _closeViewer, + icon: Icon( + Icons.arrow_back_rounded, + color: Colors.white.withValues(alpha: 0.9), + ), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + ), + const Spacer(), + IconButton( + onPressed: _toggleFullscreen, + icon: Icon( + Icons.fullscreen_rounded, + color: Colors.white.withValues(alpha: 0.9), + ), + tooltip: l10n.fullscreen, + ), + ], + ), + ), + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: SafeArea( + top: false, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 180), + opacity: _isChromeVisible ? 1 : 0, + child: IgnorePointer( + ignoring: !_isChromeVisible, + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.black54], + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (widget.url != null || widget.bytes != null) + IconButton( + onPressed: _isDownloadingMedia ? null : _shareImage, + tooltip: l10n.share, + icon: _isDownloadingMedia + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white.withValues(alpha: 0.9), + ), + ) + : Icon(Icons.share_rounded, color: Colors.white.withValues(alpha: 0.9)), + ), + if (!kIsWeb && (widget.url != null || widget.bytes != null)) + IconButton( + onPressed: (_downloaded || _isSavingMedia) ? null : _saveImage, + tooltip: l10n.save, + icon: _isSavingMedia + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white.withValues(alpha: 0.9), + ), + ) + : _downloaded + ? Icon(Icons.check_circle_rounded, color: Colors.white.withValues(alpha: 0.9)) + : Icon(Icons.download_rounded, color: Colors.white.withValues(alpha: 0.9)), + ), + if (widget.navigateToPost != null) + IconButton( + onPressed: () { + Navigator.of(context).pop(); + widget.navigateToPost?.call(); + }, + tooltip: l10n.comments, + icon: Icon(Icons.chat_rounded, color: Colors.white.withValues(alpha: 0.9)), + ), + if (widget.altText?.isNotEmpty == true) + IconButton( + onPressed: () => setState(() => _showAltText = !_showAltText), + tooltip: l10n.altText, + icon: Icon( + Icons.text_fields_rounded, + color: Colors.white.withValues(alpha: _showAltText ? 0.9 : 0.55), + ), + ), + IconButton( + onPressed: _toggleFullscreen, + tooltip: l10n.fullscreen, + icon: Icon(Icons.fullscreen_rounded, color: Colors.white.withValues(alpha: 0.9)), + ), + ], + ), + ), + ), + ), + ), + ), + if (_showAltText && widget.altText?.isNotEmpty == true) + Positioned( + bottom: kBottomNavigationBarHeight + 32, + left: 16, + right: 16, + child: SafeArea( + top: false, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 180), + opacity: _isChromeVisible ? 1 : 0, + child: IgnorePointer( + ignoring: !_isChromeVisible, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.45), + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + widget.altText!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white.withValues(alpha: 0.95), + ), + ), + ), + ), + ), + ), + ), + ), + if (!_isChromeVisible) + Positioned( + top: 0, + right: 0, + child: SafeArea( + bottom: false, + child: IconButton( + onPressed: _exitFullscreen, + tooltip: l10n.fullscreen, + icon: Icon( + Icons.fullscreen_exit_rounded, + color: Colors.white.withValues(alpha: 0.8), + ), + ), + ), + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/shared/content/widgets/media/media_view.dart b/lib/src/shared/content/widgets/media/media_view.dart index 34b6c78ec..759796e1d 100644 --- a/lib/src/shared/content/widgets/media/media_view.dart +++ b/lib/src/shared/content/widgets/media/media_view.dart @@ -15,7 +15,6 @@ import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/shared/links/widgets/link_bottom_sheet.dart'; import 'package:thunder/src/shared/content/widgets/media/image_preview.dart'; -import 'package:thunder/src/shared/content/widgets/media/image_viewer.dart'; import 'package:thunder/src/shared/content/utils/media/media_utils.dart'; import 'package:thunder/src/shared/content/widgets/media/link_information.dart'; import 'package:thunder/src/shared/content/widgets/media/media_view_text.dart'; @@ -57,6 +56,9 @@ class MediaView extends StatefulWidget { /// Whether the post has been read. final bool? read; + /// Optional callback for marking the parent post as read. + final Future Function()? onMarkPostRead; + const MediaView({ super.key, required this.media, @@ -71,6 +73,7 @@ class MediaView extends StatefulWidget { this.viewMode = ViewMode.comfortable, this.navigateToPost, this.read, + this.onMarkPostRead, }); @override @@ -109,6 +112,11 @@ class _MediaViewState extends State with TickerProviderStateMixin { void _markPostAsRead() { if (!widget.isUserLoggedIn || !widget.markPostReadOnMediaView) return; + if (widget.onMarkPostRead != null) { + widget.onMarkPostRead!(); + return; + } + try { final feedBloc = BlocProvider.of(context); feedBloc.add( @@ -196,7 +204,7 @@ class _MediaViewState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { final imagePeekDurationMs = context.select((cubit) => cubit.state.imagePeekDuration); - final tabletMode = widget.viewMode == ViewMode.comfortable ? context.select((ThunderBloc bloc) => bloc.state.tabletMode) : false; + final tabletMode = widget.viewMode == ViewMode.comfortable ? context.select((ThunderCubit bloc) => bloc.state.tabletMode) : false; final l10n = AppLocalizations.of(context)!; final imageUrlCandidate = widget.media.imageUrl ?? widget.media.mediaUrl ?? widget.media.originalUrl; @@ -307,7 +315,8 @@ class _MediaViewState extends State with TickerProviderStateMixin { builder: (context) { return FadeTransition( opacity: _overlayAnimationController, - child: ImageViewer( + child: buildImageViewerWidget( + context, url: widget.media.thumbnailUrl ?? widget.media.mediaUrl, postId: widget.postId, navigateToPost: widget.navigateToPost, diff --git a/lib/src/shared/gesture_fab.dart b/lib/src/shared/gesture_fab.dart index efd5ab069..e7917550d 100644 --- a/lib/src/shared/gesture_fab.dart +++ b/lib/src/shared/gesture_fab.dart @@ -6,7 +6,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/src/features/feed/api.dart'; +import 'package:thunder/src/app/shell/state/shell_chrome_cubit.dart'; import 'package:thunder/src/features/settings/api.dart'; /// Enum to distinguish between feed and post FABs @@ -82,13 +82,13 @@ class _GestureFabState extends State with SingleTickerProviderStateM } /// Gets the current isFabOpen state based on the fabType - bool _getIsFabOpen(FabStateState state) { + bool _getIsFabOpen(ShellChromeState state) { return widget.fabType == FabType.feed ? state.isFeedFabOpen : state.isPostFabOpen; } /// Sets the FAB open state based on the fabType void _setFabOpen(BuildContext context, bool isOpen) { - final cubit = context.read(); + final cubit = context.read(); if (widget.fabType == FabType.feed) { cubit.setFeedFabOpen(isOpen); } else { @@ -98,7 +98,7 @@ class _GestureFabState extends State with SingleTickerProviderStateM /// Sets the FAB summoned state based on the fabType void _setFabSummoned(BuildContext context, bool isSummoned) { - final cubit = context.read(); + final cubit = context.read(); if (widget.fabType == FabType.feed) { cubit.setFeedFabSummoned(isSummoned); } else { @@ -108,7 +108,7 @@ class _GestureFabState extends State with SingleTickerProviderStateM @override Widget build(BuildContext context) { - return BlocConsumer( + return BlocConsumer( listenWhen: (previous, current) => _getIsFabOpen(previous) != _getIsFabOpen(current), listener: (context, state) { final isOpen = _getIsFabOpen(state); @@ -286,7 +286,7 @@ class ActionButton extends StatelessWidget { /// Sets the FAB open state based on the fabType void _setFabOpen(BuildContext context, bool isOpen) { - final cubit = context.read(); + final cubit = context.read(); if (fabType == FabType.feed) { cubit.setFeedFabOpen(isOpen); } else { diff --git a/lib/src/shared/input_dialogs.dart b/lib/src/shared/input_dialogs.dart index d65e5a6ef..7dc20e530 100644 --- a/lib/src/shared/input_dialogs.dart +++ b/lib/src/shared/input_dialogs.dart @@ -2,19 +2,17 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:collection/collection.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/community/api.dart'; import 'package:thunder/src/features/instance/api.dart'; -import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/account/api.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/features/search/api.dart'; +import 'package:thunder/src/features/account/data/cache/profile_site_info_cache.dart'; import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; - import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/shared/marquee_widget.dart'; @@ -90,13 +88,15 @@ Future> getUserSuggestions( } Widget buildUserSuggestionWidget(BuildContext context, ThunderUser payload, {void Function(ThunderUser)? onSelected}) { + final tooltip = generateUserFullName( + context, + payload.name, + payload.displayName, + fetchInstanceNameFromUrl(payload.actorId), + ); + return Tooltip( - message: generateUserFullName( - context, - payload.name, - payload.displayName, - fetchInstanceNameFromUrl(payload.actorId), - ), + message: tooltip, preferBelow: false, child: InkWell( onTap: onSelected == null ? null : () => onSelected(payload), @@ -113,8 +113,7 @@ Widget buildUserSuggestionWidget(BuildContext context, ThunderUser payload, {voi name: payload.name, displayName: payload.displayName, instance: fetchInstanceNameFromUrl(payload.actorId), - // Override because we're showing display name above - useDisplayName: false, + useDisplayName: false, // Override because we're showing display name above ), ), ), @@ -123,28 +122,24 @@ Widget buildUserSuggestionWidget(BuildContext context, ThunderUser payload, {voi ); } -/// Shows a dialog which allows typing/search for a community. -/// Given an [account], the dialog will show subscriptions and favorites of that account. -/// -/// When searching for communities, it will use the provided [account]'s instance. +/// Shows a dialog which allows typing/search for a community. Favourited and subscribed communities are prioritized in suggestions. void showCommunityInputDialog( BuildContext context, { required String title, required Account account, required void Function(ThunderCommunity community) onCommunitySelected, - List? emptySuggestions, + List? suggestions, }) async { final l10n = GlobalContext.l10n; - List? favoritedCommunities; + List? favouritedCommunities; try { - // Fetch subscriptions from the given account - final favorites = await Favorite.favorites(account.id); + final favourites = await Favorite.favorites(account.id); final subscriptions = await AccountRepositoryImpl(account: account).subscriptions(); - favoritedCommunities = subscriptions.where((community) => favorites.any((favorite) => favorite.communityId == community.id)).toList(); + favouritedCommunities = subscriptions.where((community) => favourites.any((favorite) => favorite.communityId == community.id)).toList(); - emptySuggestions ??= prioritizeFavorites(subscriptions, favoritedCommunities); + suggestions ??= prioritizeFavorites(subscriptions, favouritedCommunities); } catch (e) { // If this is unavailable, continue } @@ -184,8 +179,8 @@ void showCommunityInputDialog( primaryButtonText: l10n.ok, secondaryButtonText: l10n.cancel, onSubmitted: onSubmitted, - getSuggestions: (query) => getCommunitySuggestions(context, query: query, account: account, emptySuggestions: emptySuggestions, favoritedCommunities: favoritedCommunities), - suggestionBuilder: (payload) => buildCommunitySuggestionWidget(context, payload), + getSuggestions: (query) => getCommunitySuggestions(context, query: query, account: account, suggestions: suggestions, favouritedCommunities: favouritedCommunities), + suggestionBuilder: (payload) => buildCommunitySuggestionWidget(context, payload, favouriteCommunityIds: favouritedCommunities?.map((community) => community.id).toSet()), ); } @@ -193,10 +188,10 @@ Future> getCommunitySuggestions( BuildContext context, { required String query, required Account account, - List? favoritedCommunities, - List? emptySuggestions, + List? favouritedCommunities, + List? suggestions, }) async { - if (query.isEmpty) return emptySuggestions ?? []; + if (query.isEmpty) return suggestions ?? []; final response = await SearchRepositoryImpl(account: account).search( query: query, @@ -205,19 +200,21 @@ Future> getCommunitySuggestions( sort: SearchSortType.topAll, ); - return prioritizeFavorites(response.communities, favoritedCommunities) ?? []; + return prioritizeFavorites(response.communities, favouritedCommunities) ?? []; } -Widget buildCommunitySuggestionWidget(BuildContext context, ThunderCommunity payload, {void Function(ThunderCommunity)? onSelected}) { +Widget buildCommunitySuggestionWidget(BuildContext context, ThunderCommunity payload, {Set? favouriteCommunityIds, void Function(ThunderCommunity)? onSelected}) { final l10n = GlobalContext.l10n; + final tooltip = generateCommunityFullName( + context, + payload.name, + payload.title, + fetchInstanceNameFromUrl(payload.actorId), + ); + return Tooltip( - message: generateCommunityFullName( - context, - payload.name, - payload.title, - fetchInstanceNameFromUrl(payload.actorId), - ), + message: tooltip, preferBelow: false, child: InkWell( onTap: onSelected == null ? null : () => onSelected(payload), @@ -237,8 +234,7 @@ Widget buildCommunitySuggestionWidget(BuildContext context, ThunderCommunity pay name: payload.name, displayName: payload.title, instance: fetchInstanceNameFromUrl(payload.actorId), - // Override because we're showing display name above - useDisplayName: false, + useDisplayName: false, // Override because we're showing display name above ), ), if (payload.subscribed != null && payload.subscribers != null) ...[ @@ -253,7 +249,7 @@ Widget buildCommunitySuggestionWidget(BuildContext context, ThunderCommunity pay SubscriptionStatus.notSubscribed => '', _ => '', }}'), - if (_getFavoriteStatus(context, payload)) ...[ + if (favouriteCommunityIds?.contains(payload.id) == true) ...[ Text(' · '), Icon(Icons.star_rounded, size: 15.0), ], @@ -268,38 +264,41 @@ Widget buildCommunitySuggestionWidget(BuildContext context, ThunderCommunity pay ); } -/// Checks whether the current community is a favorite of the current user -bool _getFavoriteStatus(BuildContext context, ThunderCommunity community) { - final state = context.read().state; - return state.favorites.any((c) => c.id == community.id); -} - /// Shows a dialog which allows typing/search for an instance. Federated instances are loaded in the background; suggestions appear as they load. void showInstanceInputDialog( BuildContext context, { required String title, + required Account account, required void Function(ThunderInstanceInfo) onInstanceSelected, - Iterable>? emptySuggestions, + Iterable>? suggestions, }) async { - final account = await fetchActiveProfile(); - final linkedInstances = []; - unawaited(_loadLinkedInstances(account, linkedInstances)); + final l10n = GlobalContext.l10n; + + final instances = []; + unawaited(_loadLinkedInstances(account, instances)); Future onSubmitted({ThunderInstanceInfo? payload, String? value}) async { + if (payload == null && value == null) return null; + if (payload != null) { onInstanceSelected(payload); Navigator.of(context).pop(); - } else if (value != null && value.trim().isNotEmpty) { + return null; + } + + if (value != null && value.trim().isNotEmpty) { final trimmed = value.trim(); - final instance = linkedInstances.firstWhereOrNull((ThunderInstanceInfo i) => i.domain == trimmed); + final instance = instances.firstWhereOrNull((i) => i.domain == trimmed); if (instance != null) { onInstanceSelected(instance); Navigator.of(context).pop(); - } else { - onInstanceSelected(ThunderInstanceInfo(domain: trimmed, name: trimmed)); - Navigator.of(context).pop(); + return null; } + + onInstanceSelected(ThunderInstanceInfo(domain: trimmed, name: trimmed)); + Navigator.of(context).pop(); + return null; } return null; @@ -309,46 +308,42 @@ void showInstanceInputDialog( showThunderTypeaheadDialog( context: context, title: title, - inputLabel: AppLocalizations.of(context)!.instance(1), - primaryButtonText: AppLocalizations.of(context)!.ok, - secondaryButtonText: AppLocalizations.of(context)!.cancel, + inputLabel: l10n.instance(1), + primaryButtonText: l10n.ok, + secondaryButtonText: l10n.cancel, onSubmitted: onSubmitted, - getSuggestions: (query) => getInstanceSuggestions(query, linkedInstances), - suggestionBuilder: (payload) => buildInstanceSuggestionWidget(payload, context: context), + getSuggestions: (query) => getInstanceSuggestions(query, instances), + suggestionBuilder: (payload) => buildInstanceSuggestionWidget(context, payload, onSelected: (instance) => onSubmitted(payload: instance)), ); } } -Future _loadLinkedInstances(Account? account, List out) async { - if (account == null) return; - +Future _loadLinkedInstances(Account account, List out) async { try { - final response = await InstanceRepositoryImpl(account: account).federated(); - final List parsed = List.from( - (response['federated_instances']['linked'] as List).map( + final federated = await InstanceRepositoryImpl(account: account).federated(); + final linked = List.from( + (federated['federated_instances']['linked'] as List).map( (instance) => ThunderInstanceInfo(id: instance['id'], domain: instance['domain'], name: instance['domain']), ), ); out ..clear() - ..addAll(parsed); + ..addAll(linked); } catch (_) { // Dialog still works with empty list; user can type a domain and submit. } } -Future> getInstanceSuggestions(String query, List? emptySuggestions) async { - if (query.isEmpty) { - return []; - } +Future> getInstanceSuggestions(String query, List? suggestions) async { + if (query.isEmpty) return suggestions ?? []; - List filteredInstances = emptySuggestions?.where((ThunderInstanceInfo instance) => instance.domain.contains(query)).toList() ?? [] as List; - return filteredInstances; + final instances = suggestions?.where((instance) => instance.domain.contains(query)).toList() ?? [] as List; + return instances; } -Widget buildInstanceSuggestionWidget(ThunderInstanceInfo payload, {void Function(ThunderInstanceInfo)? onSelected, BuildContext? context}) { - final theme = Theme.of(context!); +Widget buildInstanceSuggestionWidget(BuildContext context, ThunderInstanceInfo payload, {void Function(ThunderInstanceInfo)? onSelected}) { + final theme = Theme.of(context); return Tooltip( message: payload.domain, @@ -379,31 +374,52 @@ Widget buildInstanceSuggestionWidget(ThunderInstanceInfo payload, {void Function } /// Shows a dialog which allows typing/search for an language -void showLanguageInputDialog(BuildContext context, - {required String title, required void Function(ThunderLanguage) onLanguageSelected, Iterable? excludedLanguageIds, Iterable? emptySuggestions}) async { - ProfileState state = context.read().state; - final AppLocalizations l10n = AppLocalizations.of(context)!; +void showLanguageInputDialog( + BuildContext context, { + required String title, + required Account account, + required void Function(ThunderLanguage) onLanguageSelected, + Iterable? excludedLanguageIds, + Iterable? suggestions, +}) async { + final l10n = GlobalContext.l10n; + + List languages = suggestions?.toList() ?? const []; - List languages = [ThunderLanguage(id: -1, code: '', name: l10n.noLanguage), ...(state.siteResponse?.allLanguages ?? [])]; + if (languages.isEmpty) { + final site = await ProfileSiteInfoCache.instance.get(account); + languages = site.allLanguages ?? const []; + } + + languages = [ThunderLanguage(id: -1, code: '', name: l10n.noLanguage), ...languages]; + + // Exclude languages with IDs in excludedLanguageIds languages = languages.where((language) { if (excludedLanguageIds != null && excludedLanguageIds.isNotEmpty) { return !excludedLanguageIds.contains(language.id); } + return true; }).toList(); Future onSubmitted({ThunderLanguage? payload, String? value}) async { + if (payload == null && value == null) return null; + if (payload != null) { onLanguageSelected(payload); Navigator.of(context).pop(); - } else if (value != null) { - final ThunderLanguage? language = languages.firstWhereOrNull((ThunderLanguage language) => language.name.toLowerCase().contains(value.toLowerCase())); + return null; + } + + if (value != null) { + final language = languages.firstWhereOrNull((language) => language.name.toLowerCase().contains(value.toLowerCase())); if (language != null) { onLanguageSelected(language); Navigator.of(context).pop(); + return null; } else { - return AppLocalizations.of(context)!.unableToFindLanguage; + return l10n.unableToFindLanguage; } } @@ -414,35 +430,36 @@ void showLanguageInputDialog(BuildContext context, showThunderTypeaheadDialog( context: context, title: title, - inputLabel: AppLocalizations.of(context)!.language, - primaryButtonText: AppLocalizations.of(context)!.ok, - secondaryButtonText: AppLocalizations.of(context)!.cancel, + inputLabel: l10n.language, + primaryButtonText: l10n.ok, + secondaryButtonText: l10n.cancel, onSubmitted: onSubmitted, getSuggestions: (query) => getLanguageSuggestions(context, query, languages), - suggestionBuilder: (payload) => buildLanguageSuggestionWidget(payload, context: context), + suggestionBuilder: (payload) => buildLanguageSuggestionWidget(context, payload), ); } } -Future> getLanguageSuggestions(BuildContext context, String query, List? emptySuggestions) async { - final Locale currentLocale = Localizations.localeOf(context); +Future> getLanguageSuggestions(BuildContext context, String query, List? suggestions) async { + final currentLocale = Localizations.localeOf(context); + final currentLanguage = suggestions?.firstWhereOrNull((l) => l.code == currentLocale.languageCode); - final ThunderLanguage? currentLanguage = emptySuggestions?.firstWhereOrNull((ThunderLanguage l) => l.code == currentLocale.languageCode); - if (currentLanguage != null && (emptySuggestions?.length ?? 0) >= 2) { - emptySuggestions = emptySuggestions?.toList() + // Move the current language to the top of the suggestions list. + if (currentLanguage != null && (suggestions?.length ?? 0) >= 2) { + suggestions = suggestions?.toList() ?..remove(currentLanguage) ..insert(2, currentLanguage); } if (query.isEmpty) { - return emptySuggestions ?? []; + return suggestions ?? []; } - List filteredLanguages = emptySuggestions?.where((ThunderLanguage language) => language.name.toLowerCase().contains(query.toLowerCase())).toList() ?? []; - return filteredLanguages; + final languages = suggestions?.where((language) => language.name.toLowerCase().contains(query.toLowerCase())).toList() ?? []; + return languages; } -Widget buildLanguageSuggestionWidget(ThunderLanguage payload, {void Function(ThunderLanguage)? onSelected, BuildContext? context}) { +Widget buildLanguageSuggestionWidget(BuildContext context, ThunderLanguage payload, {void Function(ThunderLanguage)? onSelected}) { return Tooltip( message: payload.name, preferBelow: false, @@ -460,19 +477,27 @@ Widget buildLanguageSuggestionWidget(ThunderLanguage payload, {void Function(Thu } /// Shows a dialog which allows typing/search for a keyword -void showKeywordInputDialog(BuildContext context, {required String title, required void Function(String) onKeywordSelected}) async { - final l10n = AppLocalizations.of(context)!; +void showKeywordInputDialog( + BuildContext context, { + required String title, + required void Function(String) onKeywordSelected, +}) async { + final l10n = GlobalContext.l10n; Future onSubmitted({String? payload, String? value}) async { - String? formattedPayload = payload?.trim(); - String? formattedValue = value?.trim(); + final formattedPayload = payload?.trim(); + final formattedValue = value?.trim(); if (formattedPayload != null && formattedPayload.isNotEmpty) { onKeywordSelected(formattedPayload); Navigator.of(context).pop(); - } else if (formattedValue != null && formattedValue.isNotEmpty) { + return null; + } + + if (formattedValue != null && formattedValue.isNotEmpty) { onKeywordSelected(formattedValue); Navigator.of(context).pop(); + return null; } return null; diff --git a/lib/src/shared/language_selector.dart b/lib/src/shared/language_selector.dart index f73caf31f..4eb48badf 100644 --- a/lib/src/shared/language_selector.dart +++ b/lib/src/shared/language_selector.dart @@ -1,31 +1,33 @@ -// Flutter imports import 'package:flutter/material.dart'; -// Package imports import 'package:collection/collection.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/l10n/generated/app_localizations.dart'; -// Project imports -import 'package:thunder/src/features/account/api.dart'; +import 'package:thunder/src/features/account/account.dart'; +import 'package:thunder/src/features/account/data/cache/profile_site_info_cache.dart'; +import 'package:thunder/src/foundation/config/global_context.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/shared/input_dialogs.dart'; -/// Creates a widget which displays a preview of a pre-selected language, with the ability to change the selected language -/// -/// Passing in [languageId] will set the initial state of the widget to display that given language. -/// A callback function [onLanguageSelected] will be triggered whenever a new language is selected from the dropdown. +/// Creates a widget which displays a preview of a pre-selected language, with the ability to change the selected language. class LanguageSelector extends StatefulWidget { const LanguageSelector({ super.key, - required this.languageId, + required this.account, + this.languages, + this.languageId, required this.onLanguageSelected, }); - /// The initial language id to be passed in + /// Account used to determine available languages. + final Account account; + + /// List of preloaded languages. If available, will use this instead of fetching languages from the account. + final Iterable? languages; + + /// The initial language id to be passed in. final int? languageId; - /// A callback function to trigger whenever a language is selected from the dropdown + /// A callback function to trigger whenever a language is selected from the dropdown. final Function(ThunderLanguage?) onLanguageSelected; @override @@ -33,20 +35,52 @@ class LanguageSelector extends StatefulWidget { } class _LanguageSelectorState extends State { + List _languages = const []; + + @override + void initState() { + super.initState(); + _load(); + } + + @override + void didUpdateWidget(covariant LanguageSelector oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.account.id != widget.account.id || oldWidget.account.instance != widget.account.instance || oldWidget.languages != widget.languages) { + _load(); + } + } + + Future _load() async { + if (widget.languages != null) { + if (!mounted) return; + setState(() => _languages = widget.languages!.toList()); + return; + } + + final site = await ProfileSiteInfoCache.instance.get(widget.account); + if (!mounted) return; + setState(() => _languages = site.allLanguages ?? const []); + return; + } + @override Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; + final l10n = GlobalContext.l10n; final theme = Theme.of(context); - final languages = context.read().state.siteResponse?.allLanguages ?? []; - final language = languages.firstWhereOrNull((ThunderLanguage candidate) => candidate.id == widget.languageId); + + final languages = widget.languages?.toList() ?? _languages; + final language = languages.firstWhereOrNull((candidate) => candidate.id == widget.languageId); return Transform.translate( - offset: const Offset(-8, 0), + offset: const Offset(-8.0, 0.0), child: InkWell( onTap: () { showLanguageInputDialog( context, title: l10n.language, + account: widget.account, + suggestions: languages, onLanguageSelected: (language) { if (language.id == -1) { widget.onLanguageSelected(null); @@ -58,11 +92,11 @@ class _LanguageSelectorState extends State { }, borderRadius: const BorderRadius.all(Radius.circular(50)), child: Padding( - padding: const EdgeInsets.only(left: 8, right: 8, top: 12, bottom: 12), + padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 12.0, bottom: 12.0), child: Text.rich( softWrap: true, TextSpan( - children: [ + children: [ TextSpan(text: language != null ? '${l10n.language}: ${language.name}' : l10n.selectLanguage), const WidgetSpan( alignment: PlaceholderAlignment.middle, diff --git a/lib/src/shared/share/share_image_preview.dart b/lib/src/shared/share/share_image_preview.dart index 18d584d6e..590b3e72f 100644 --- a/lib/src/shared/share/share_image_preview.dart +++ b/lib/src/shared/share/share_image_preview.dart @@ -91,7 +91,7 @@ class _ShareImagePreviewState extends State { Widget imagePreview(BuildContext context) { final ThemeData theme = Theme.of(context); final AppLocalizations l10n = AppLocalizations.of(context)!; - final ThunderState thunderState = context.read().state; + final ThunderState thunderState = context.read().state; return Container( clipBehavior: Clip.hardEdge, diff --git a/pubspec.lock b/pubspec.lock index ae77c6e69..450885ff5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" url: "https://pub.dev" source: hosted - version: "91.0.0" + version: "93.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b url: "https://pub.dev" source: hosted - version: "8.4.1" + version: "10.0.1" android_intent_plus: dependency: "direct main" description: @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + bloc_lint: + dependency: "direct dev" + description: + name: bloc_lint + sha256: e40b578297488e5a263cbfa2180b186546f32da40ae8a57cad6aa68be5a46f79 + url: "https://pub.dev" + source: hosted + version: "0.4.0" bloc_test: dependency: "direct dev" description: @@ -221,10 +229,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -365,10 +373,10 @@ packages: dependency: transitive description: name: dart_style - sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.7" dbus: dependency: transitive description: @@ -921,10 +929,10 @@ packages: dependency: "direct main" description: name: freezed - sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" + sha256: f23ea33b3863f119b58ed1b586e881a46bd28715ddcc4dbc33104524e3434131 url: "https://pub.dev" source: hosted - version: "3.2.3" + version: "3.2.5" freezed_annotation: dependency: "direct main" description: @@ -1236,18 +1244,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -1536,6 +1544,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + pubspec_lock_parse: + dependency: transitive + description: + name: pubspec_lock_parse + sha256: "020cb470287124c936c30ebfc2f927b287f275b7bf7fc2ab11577e592c017764" + url: "https://pub.dev" + source: hosted + version: "2.2.0" pubspec_parse: dependency: transitive description: @@ -1857,26 +1873,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.30.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.16" timezone: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 739225f95..7f1689217 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -101,6 +101,7 @@ dev_dependencies: mocktail: ^1.0.4 sqflite: ^2.4.0 sqflite_common_ffi: ^2.3.4 + bloc_lint: ^0.4.0 flutter: uses-material-design: true diff --git a/test/app/deep_links_cubit_test.dart b/test/app/deep_links_cubit_test.dart deleted file mode 100644 index 6eae40987..000000000 --- a/test/app/deep_links_cubit_test.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/src/app/state/deep_links_cubit/deep_links_cubit.dart'; -import 'package:thunder/src/app/shell/routing/deep_link_enums.dart'; -import 'package:thunder/src/foundation/contracts/deep_link_service.dart'; -import 'package:thunder/src/foundation/contracts/localization_service.dart'; -import 'package:thunder/src/foundation/errors/app_error_reason.dart'; - -class _FakeDeepLinkService implements DeepLinkService { - _FakeDeepLinkService(this._stream); - - final Stream _stream; - - @override - Stream get uriLinkStream => _stream; -} - -class _MockLocalizationService extends Mock implements LocalizationService {} - -class _MockAppLocalizations extends Mock implements AppLocalizations {} - -void main() { - group('DeepLinksCubit', () { - late _MockLocalizationService localizationService; - late _MockAppLocalizations l10n; - - setUp(() { - localizationService = _MockLocalizationService(); - l10n = _MockAppLocalizations(); - when(() => localizationService.l10n).thenReturn(l10n); - when(() => l10n.uriNotSupported).thenReturn('URI not supported'); - }); - - blocTest( - 'emits success for user links', - build: () => DeepLinksCubit( - deepLinkService: _FakeDeepLinkService(const Stream.empty()), - localizationService: localizationService, - ), - act: (cubit) => cubit.getLinkType('https://lemmy.world/u/tester'), - expect: () => [ - isA() - .having((state) => state.deepLinkStatus, 'status', - DeepLinkStatus.success) - .having((state) => state.linkType, 'type', LinkType.user) - .having( - (state) => state.link, 'link', 'https://lemmy.world/u/tester'), - ], - ); - - blocTest( - 'emits typed validation error for unsupported links', - build: () => DeepLinksCubit( - deepLinkService: _FakeDeepLinkService(const Stream.empty()), - localizationService: localizationService, - ), - act: (cubit) => cubit.getLinkType('https://lemmy.world/random/path'), - expect: () => [ - isA() - .having((state) => state.deepLinkStatus, 'status', - DeepLinkStatus.unknown) - .having((state) => state.errorReason?.category, 'error category', - AppErrorCategory.validation), - ], - ); - }); -} diff --git a/test/app/state_copy_with_nullability_test.dart b/test/app/state_copy_with_nullability_test.dart deleted file mode 100644 index be64038e2..000000000 --- a/test/app/state_copy_with_nullability_test.dart +++ /dev/null @@ -1,247 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:intl/intl.dart'; -import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; -import 'package:thunder/src/app/state/deep_links_cubit/deep_links_cubit.dart'; -import 'package:thunder/src/app/shell/routing/deep_link_enums.dart'; -import 'package:thunder/src/features/feed/api.dart'; -import 'package:thunder/src/features/notification/application/state/notifications_cubit.dart'; -import 'package:thunder/src/foundation/errors/app_error_reason.dart'; -import 'package:thunder/src/features/comment/presentation/state/create_comment_cubit.dart'; -import 'package:thunder/src/features/inbox/presentation/state/inbox_bloc.dart'; -import 'package:thunder/src/features/instance/presentation/state/instance_page_bloc.dart'; -import 'package:thunder/src/features/moderator/presentation/state/report_bloc.dart'; -import 'package:thunder/src/features/post/presentation/state/create_post_cubit.dart'; -import 'package:thunder/src/features/search/presentation/state/search_bloc.dart'; - -void main() { - group('Nullable copyWith semantics', () { - test('ThunderState preserves and clears nullable fields explicitly', () { - const state = ThunderState( - errorMessage: 'error', - appLanguageCode: 'en', - currentAnonymousInstance: 'lemmy.world', - ); - - final preserved = state.copyWith(status: ThunderStatus.success); - expect(preserved.errorMessage, 'error'); - expect(preserved.appLanguageCode, 'en'); - expect(preserved.currentAnonymousInstance, 'lemmy.world'); - - final cleared = state.copyWith( - errorMessage: null, - appLanguageCode: null, - currentAnonymousInstance: null, - ); - expect(cleared.errorMessage, isNull); - expect(cleared.appLanguageCode, isNull); - expect(cleared.currentAnonymousInstance, isNull); - expect(cleared.errorReason, isNull); - }); - - test('FeedUiState preserves dismiss IDs when unrelated fields update', () { - const state = FeedUiState( - dismissBlockedUserId: 10, - dismissBlockedCommunityId: 20, - dismissHiddenPostId: 30, - ); - - final preserved = state.copyWith(scrollId: 1); - expect(preserved.dismissBlockedUserId, 10); - expect(preserved.dismissBlockedCommunityId, 20); - expect(preserved.dismissHiddenPostId, 30); - - final cleared = state.copyWith( - dismissBlockedUserId: null, - dismissBlockedCommunityId: null, - dismissHiddenPostId: null, - ); - expect(cleared.dismissBlockedUserId, isNull); - expect(cleared.dismissBlockedCommunityId, isNull); - expect(cleared.dismissHiddenPostId, isNull); - }); - - test('NotificationsState preserves ID/account when toggling pending', () { - const state = NotificationsState( - status: NotificationsStatus.reply, - notificationId: 4, - accountId: 'acct-1', - ); - - final pending = state.copyWith(pending: true); - expect(pending.notificationId, 4); - expect(pending.accountId, 'acct-1'); - - final cleared = state.copyWith(notificationId: null, accountId: null); - expect(cleared.notificationId, isNull); - expect(cleared.accountId, isNull); - }); - - test('InboxState preserves and clears nullable fields explicitly', () { - const state = InboxState( - status: InboxStatus.failure, - errorMessage: 'failed', - inboxReplyMarkedAsRead: 9, - ); - - final preserved = state.copyWith(status: InboxStatus.loading); - expect(preserved.errorMessage, 'failed'); - expect(preserved.inboxReplyMarkedAsRead, 9); - - final cleared = state.copyWith( - status: InboxStatus.success, - errorMessage: null, - inboxReplyMarkedAsRead: null, - ); - expect(cleared.errorMessage, isNull); - expect(cleared.inboxReplyMarkedAsRead, isNull); - }); - - test('ReportState preserves and clears nullable fields explicitly', () { - const state = ReportState( - communityId: 42, - message: 'failed', - ); - - final preserved = state.copyWith(status: ReportStatus.fetching); - expect(preserved.communityId, 42); - expect(preserved.message, 'failed'); - - final cleared = state.copyWith(communityId: null, message: null); - expect(cleared.communityId, isNull); - expect(cleared.message, isNull); - }); - - test('Instance page states preserve and clear messages explicitly', () { - const typeState = InstanceTypeState( - message: 'type-error', - errorReason: AppErrorReason.unexpected(message: 'type-error'), - ); - final typePreserved = - typeState.copyWith(status: InstancePageStatus.loading); - expect(typePreserved.message, 'type-error'); - expect(typePreserved.errorReason, isNotNull); - - final typeCleared = typeState.copyWith(message: null, errorReason: null); - expect(typeCleared.message, isNull); - expect(typeCleared.errorReason, isNull); - - const pageState = InstancePageState( - message: 'page-error', - errorReason: AppErrorReason.unexpected(message: 'page-error'), - ); - final pagePreserved = - pageState.copyWith(status: InstancePageStatus.loading); - expect(pagePreserved.message, 'page-error'); - expect(pagePreserved.errorReason, isNotNull); - - final pageCleared = pageState.copyWith(message: null, errorReason: null); - expect(pageCleared.message, isNull); - expect(pageCleared.errorReason, isNull); - }); - - test('SearchState supports both explicit null and clear flags', () { - const state = SearchState( - message: 'error', - errorReason: AppErrorReason.unexpected(message: 'error'), - communityFilter: 1, - communityFilterName: 'c/thunder', - ); - - final preserved = state.copyWith(status: SearchStatus.loading); - expect(preserved.message, 'error'); - expect(preserved.communityFilter, 1); - - final explicitCleared = state.copyWith( - message: null, - errorReason: null, - communityFilter: null, - communityFilterName: null, - ); - expect(explicitCleared.message, isNull); - expect(explicitCleared.errorReason, isNull); - expect(explicitCleared.communityFilter, isNull); - expect(explicitCleared.communityFilterName, isNull); - - final flagCleared = state.copyWith(clearCommunityFilter: true); - expect(flagCleared.communityFilter, isNull); - expect(flagCleared.communityFilterName, isNull); - }); - - test('FeedPreferencesState preserves and clears nullable fields explicitly', - () { - final dateFormat = DateFormat.yMd(); - final state = FeedPreferencesState( - dateFormat: dateFormat, - feedCardDividerColor: const Color(0xff123456), - ); - - final preserved = state.copyWith(showHiddenPosts: true); - expect(identical(preserved.dateFormat, dateFormat), isTrue); - expect(preserved.feedCardDividerColor, const Color(0xff123456)); - - final cleared = state.copyWith( - dateFormat: null, - feedCardDividerColor: null, - ); - expect(cleared.dateFormat, isNull); - expect(cleared.feedCardDividerColor, isNull); - }); - - test('DeepLinksState preserves and clears nullable typed error explicitly', - () { - const state = DeepLinksState( - deepLinkStatus: DeepLinkStatus.error, - error: 'bad', - errorReason: AppErrorReason.validation(message: 'bad'), - ); - - final preserved = state.copyWith(linkType: LinkType.user); - expect(preserved.error, 'bad'); - expect(preserved.errorReason, isNotNull); - - final cleared = state.copyWith(error: null, errorReason: null); - expect(cleared.error, isNull); - expect(cleared.errorReason, isNull); - }); - - test('CreatePostState supports explicit null clear for typed errors', () { - const state = CreatePostState( - status: CreatePostStatus.error, - message: 'failed', - errorReason: AppErrorReason.actionFailed(message: 'failed'), - ); - - final preserved = state.copyWith(status: CreatePostStatus.loading); - expect(preserved.errorReason, isNotNull); - - final cleared = state.copyWith( - status: CreatePostStatus.initial, - message: null, - errorReason: null, - ); - expect(cleared.message, isNull); - expect(cleared.errorReason, isNull); - }); - - test('CreateCommentState supports explicit null clear for typed errors', - () { - const state = CreateCommentState( - status: CreateCommentStatus.error, - message: 'failed', - errorReason: AppErrorReason.actionFailed(message: 'failed'), - ); - - final preserved = state.copyWith(status: CreateCommentStatus.loading); - expect(preserved.errorReason, isNotNull); - - final cleared = state.copyWith( - status: CreateCommentStatus.initial, - message: null, - errorReason: null, - ); - expect(cleared.message, isNull); - expect(cleared.errorReason, isNull); - }); - }); -} diff --git a/test/app/thunder_bloc_test.dart b/test/app/thunder_bloc_test.dart deleted file mode 100644 index f05aa292a..000000000 --- a/test/app/thunder_bloc_test.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; -import 'package:thunder/src/foundation/primitives/enums/local_settings.dart'; -import 'package:thunder/src/foundation/errors/app_error_reason.dart'; -import 'package:thunder/src/foundation/primitives/models/version.dart'; -import 'package:thunder/src/foundation/contracts/version_checker.dart'; - -import '../helpers/fake_preferences_store.dart'; - -class _FailingVersionChecker implements VersionChecker { - const _FailingVersionChecker(); - - @override - Future fetchLatestVersion() async { - throw Exception('network down'); - } -} - -class _SuccessVersionChecker implements VersionChecker { - const _SuccessVersionChecker(); - - @override - Future fetchLatestVersion() async { - return Version(version: '0.0.1'); - } -} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - EquatableConfig.stringify = false; - - group('ThunderBloc', () { - blocTest( - 'emits typed error reason when initialization fails', - build: () => ThunderBloc( - preferencesStore: FakePreferencesStore(), - versionChecker: const _FailingVersionChecker(), - ), - act: (bloc) => bloc.add(InitializeAppEvent()), - expect: () => [ - isA() - .having((state) => state.status, 'status', ThunderStatus.failure) - .having((state) => state.errorReason?.category, 'category', - AppErrorCategory.unexpected), - ], - ); - - blocTest( - 'clears error reason on successful preference load', - build: () => ThunderBloc( - preferencesStore: FakePreferencesStore(settings: { - LocalSettings.currentAnonymousInstance: 'lemmy.world', - }), - versionChecker: const _SuccessVersionChecker(), - ), - seed: () => const ThunderState( - status: ThunderStatus.failure, - errorMessage: 'old', - errorReason: AppErrorReason.unexpected(message: 'old'), - ), - act: (bloc) => bloc.add(UserPreferencesChangeEvent()), - expect: () => [ - isA().having( - (state) => state.status, 'status', ThunderStatus.refreshing), - isA() - .having((state) => state.status, 'status', ThunderStatus.success) - .having((state) => state.errorReason, 'errorReason', isNull), - ], - ); - }); -} diff --git a/test/drift/thunder/generated/schema.dart b/test/drift/thunder/generated/schema.dart deleted file mode 100644 index f76d308b7..000000000 --- a/test/drift/thunder/generated/schema.dart +++ /dev/null @@ -1,42 +0,0 @@ -// dart format width=80 -// GENERATED BY drift_dev, DO NOT MODIFY. -// ignore_for_file: type=lint,unused_import -// -import 'package:drift/drift.dart'; -import 'package:drift/internal/migrations.dart'; -import 'schema_v1.dart' as v1; -import 'schema_v2.dart' as v2; -import 'schema_v3.dart' as v3; -import 'schema_v4.dart' as v4; -import 'schema_v5.dart' as v5; -import 'schema_v6.dart' as v6; -import 'schema_v7.dart' as v7; -import 'schema_v8.dart' as v8; - -class GeneratedHelper implements SchemaInstantiationHelper { - @override - GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { - switch (version) { - case 1: - return v1.DatabaseAtV1(db); - case 2: - return v2.DatabaseAtV2(db); - case 3: - return v3.DatabaseAtV3(db); - case 4: - return v4.DatabaseAtV4(db); - case 5: - return v5.DatabaseAtV5(db); - case 6: - return v6.DatabaseAtV6(db); - case 7: - return v7.DatabaseAtV7(db); - case 8: - return v8.DatabaseAtV8(db); - default: - throw MissingSchemaException(version, versions); - } - } - - static const versions = const [1, 2, 3, 4, 5, 6, 7, 8]; -} diff --git a/test/drift/thunder/generated/schema_v1.dart b/test/drift/thunder/generated/schema_v1.dart deleted file mode 100644 index 5e6d401c4..000000000 --- a/test/drift/thunder/generated/schema_v1.dart +++ /dev/null @@ -1,749 +0,0 @@ -// dart format width=80 -// GENERATED BY drift_dev, DO NOT MODIFY. -// ignore_for_file: type=lint,unused_import -// -import 'package:drift/drift.dart'; - -class Accounts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Accounts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn jwt = GeneratedColumn( - 'jwt', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn instance = GeneratedColumn( - 'instance', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn anonymous = GeneratedColumn( - 'anonymous', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("anonymous" IN (0, 1))'), - defaultValue: const CustomExpression('0')); - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - @override - List get $columns => - [id, username, jwt, instance, anonymous, userId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'accounts'; - @override - Set get $primaryKey => {id}; - @override - AccountsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AccountsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username']), - jwt: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}jwt']), - instance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}instance']), - anonymous: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}anonymous'])!, - userId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}user_id']), - ); - } - - @override - Accounts createAlias(String alias) { - return Accounts(attachedDatabase, alias); - } -} - -class AccountsData extends DataClass implements Insertable { - final int id; - final String? username; - final String? jwt; - final String? instance; - final bool anonymous; - final int? userId; - const AccountsData( - {required this.id, - this.username, - this.jwt, - this.instance, - required this.anonymous, - this.userId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - if (!nullToAbsent || username != null) { - map['username'] = Variable(username); - } - if (!nullToAbsent || jwt != null) { - map['jwt'] = Variable(jwt); - } - if (!nullToAbsent || instance != null) { - map['instance'] = Variable(instance); - } - map['anonymous'] = Variable(anonymous); - if (!nullToAbsent || userId != null) { - map['user_id'] = Variable(userId); - } - return map; - } - - AccountsCompanion toCompanion(bool nullToAbsent) { - return AccountsCompanion( - id: Value(id), - username: username == null && nullToAbsent - ? const Value.absent() - : Value(username), - jwt: jwt == null && nullToAbsent ? const Value.absent() : Value(jwt), - instance: instance == null && nullToAbsent - ? const Value.absent() - : Value(instance), - anonymous: Value(anonymous), - userId: - userId == null && nullToAbsent ? const Value.absent() : Value(userId), - ); - } - - factory AccountsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AccountsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - jwt: serializer.fromJson(json['jwt']), - instance: serializer.fromJson(json['instance']), - anonymous: serializer.fromJson(json['anonymous']), - userId: serializer.fromJson(json['userId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'jwt': serializer.toJson(jwt), - 'instance': serializer.toJson(instance), - 'anonymous': serializer.toJson(anonymous), - 'userId': serializer.toJson(userId), - }; - } - - AccountsData copyWith( - {int? id, - Value username = const Value.absent(), - Value jwt = const Value.absent(), - Value instance = const Value.absent(), - bool? anonymous, - Value userId = const Value.absent()}) => - AccountsData( - id: id ?? this.id, - username: username.present ? username.value : this.username, - jwt: jwt.present ? jwt.value : this.jwt, - instance: instance.present ? instance.value : this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId.present ? userId.value : this.userId, - ); - AccountsData copyWithCompanion(AccountsCompanion data) { - return AccountsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - jwt: data.jwt.present ? data.jwt.value : this.jwt, - instance: data.instance.present ? data.instance.value : this.instance, - anonymous: data.anonymous.present ? data.anonymous.value : this.anonymous, - userId: data.userId.present ? data.userId.value : this.userId, - ); - } - - @override - String toString() { - return (StringBuffer('AccountsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(id, username, jwt, instance, anonymous, userId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AccountsData && - other.id == this.id && - other.username == this.username && - other.jwt == this.jwt && - other.instance == this.instance && - other.anonymous == this.anonymous && - other.userId == this.userId); -} - -class AccountsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value jwt; - final Value instance; - final Value anonymous; - final Value userId; - const AccountsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - }); - AccountsCompanion.insert({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - }); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? jwt, - Expression? instance, - Expression? anonymous, - Expression? userId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (jwt != null) 'jwt': jwt, - if (instance != null) 'instance': instance, - if (anonymous != null) 'anonymous': anonymous, - if (userId != null) 'user_id': userId, - }); - } - - AccountsCompanion copyWith( - {Value? id, - Value? username, - Value? jwt, - Value? instance, - Value? anonymous, - Value? userId}) { - return AccountsCompanion( - id: id ?? this.id, - username: username ?? this.username, - jwt: jwt ?? this.jwt, - instance: instance ?? this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId ?? this.userId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (jwt.present) { - map['jwt'] = Variable(jwt.value); - } - if (instance.present) { - map['instance'] = Variable(instance.value); - } - if (anonymous.present) { - map['anonymous'] = Variable(anonymous.value); - } - if (userId.present) { - map['user_id'] = Variable(userId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AccountsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId') - ..write(')')) - .toString(); - } -} - -class Favorites extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Favorites(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - late final GeneratedColumn communityId = GeneratedColumn( - 'community_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, accountId, communityId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'favorites'; - @override - Set get $primaryKey => {id}; - @override - FavoritesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return FavoritesData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - accountId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}account_id'])!, - communityId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}community_id'])!, - ); - } - - @override - Favorites createAlias(String alias) { - return Favorites(attachedDatabase, alias); - } -} - -class FavoritesData extends DataClass implements Insertable { - final int id; - final int accountId; - final int communityId; - const FavoritesData( - {required this.id, required this.accountId, required this.communityId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['account_id'] = Variable(accountId); - map['community_id'] = Variable(communityId); - return map; - } - - FavoritesCompanion toCompanion(bool nullToAbsent) { - return FavoritesCompanion( - id: Value(id), - accountId: Value(accountId), - communityId: Value(communityId), - ); - } - - factory FavoritesData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return FavoritesData( - id: serializer.fromJson(json['id']), - accountId: serializer.fromJson(json['accountId']), - communityId: serializer.fromJson(json['communityId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'accountId': serializer.toJson(accountId), - 'communityId': serializer.toJson(communityId), - }; - } - - FavoritesData copyWith({int? id, int? accountId, int? communityId}) => - FavoritesData( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - FavoritesData copyWithCompanion(FavoritesCompanion data) { - return FavoritesData( - id: data.id.present ? data.id.value : this.id, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - communityId: - data.communityId.present ? data.communityId.value : this.communityId, - ); - } - - @override - String toString() { - return (StringBuffer('FavoritesData(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, accountId, communityId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is FavoritesData && - other.id == this.id && - other.accountId == this.accountId && - other.communityId == this.communityId); -} - -class FavoritesCompanion extends UpdateCompanion { - final Value id; - final Value accountId; - final Value communityId; - const FavoritesCompanion({ - this.id = const Value.absent(), - this.accountId = const Value.absent(), - this.communityId = const Value.absent(), - }); - FavoritesCompanion.insert({ - this.id = const Value.absent(), - required int accountId, - required int communityId, - }) : accountId = Value(accountId), - communityId = Value(communityId); - static Insertable custom({ - Expression? id, - Expression? accountId, - Expression? communityId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (accountId != null) 'account_id': accountId, - if (communityId != null) 'community_id': communityId, - }); - } - - FavoritesCompanion copyWith( - {Value? id, Value? accountId, Value? communityId}) { - return FavoritesCompanion( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (communityId.present) { - map['community_id'] = Variable(communityId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('FavoritesCompanion(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } -} - -class LocalSubscriptions extends Table - with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - LocalSubscriptions(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn actorId = GeneratedColumn( - 'actor_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn icon = GeneratedColumn( - 'icon', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => [id, name, title, actorId, icon]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'local_subscriptions'; - @override - Set get $primaryKey => {id}; - @override - LocalSubscriptionsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return LocalSubscriptionsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title'])!, - actorId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}actor_id'])!, - icon: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}icon']), - ); - } - - @override - LocalSubscriptions createAlias(String alias) { - return LocalSubscriptions(attachedDatabase, alias); - } -} - -class LocalSubscriptionsData extends DataClass - implements Insertable { - final int id; - final String name; - final String title; - final String actorId; - final String? icon; - const LocalSubscriptionsData( - {required this.id, - required this.name, - required this.title, - required this.actorId, - this.icon}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['title'] = Variable(title); - map['actor_id'] = Variable(actorId); - if (!nullToAbsent || icon != null) { - map['icon'] = Variable(icon); - } - return map; - } - - LocalSubscriptionsCompanion toCompanion(bool nullToAbsent) { - return LocalSubscriptionsCompanion( - id: Value(id), - name: Value(name), - title: Value(title), - actorId: Value(actorId), - icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon), - ); - } - - factory LocalSubscriptionsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return LocalSubscriptionsData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - title: serializer.fromJson(json['title']), - actorId: serializer.fromJson(json['actorId']), - icon: serializer.fromJson(json['icon']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'title': serializer.toJson(title), - 'actorId': serializer.toJson(actorId), - 'icon': serializer.toJson(icon), - }; - } - - LocalSubscriptionsData copyWith( - {int? id, - String? name, - String? title, - String? actorId, - Value icon = const Value.absent()}) => - LocalSubscriptionsData( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon.present ? icon.value : this.icon, - ); - LocalSubscriptionsData copyWithCompanion(LocalSubscriptionsCompanion data) { - return LocalSubscriptionsData( - id: data.id.present ? data.id.value : this.id, - name: data.name.present ? data.name.value : this.name, - title: data.title.present ? data.title.value : this.title, - actorId: data.actorId.present ? data.actorId.value : this.actorId, - icon: data.icon.present ? data.icon.value : this.icon, - ); - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, title, actorId, icon); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is LocalSubscriptionsData && - other.id == this.id && - other.name == this.name && - other.title == this.title && - other.actorId == this.actorId && - other.icon == this.icon); -} - -class LocalSubscriptionsCompanion - extends UpdateCompanion { - final Value id; - final Value name; - final Value title; - final Value actorId; - final Value icon; - const LocalSubscriptionsCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.title = const Value.absent(), - this.actorId = const Value.absent(), - this.icon = const Value.absent(), - }); - LocalSubscriptionsCompanion.insert({ - this.id = const Value.absent(), - required String name, - required String title, - required String actorId, - this.icon = const Value.absent(), - }) : name = Value(name), - title = Value(title), - actorId = Value(actorId); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? title, - Expression? actorId, - Expression? icon, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (title != null) 'title': title, - if (actorId != null) 'actor_id': actorId, - if (icon != null) 'icon': icon, - }); - } - - LocalSubscriptionsCompanion copyWith( - {Value? id, - Value? name, - Value? title, - Value? actorId, - Value? icon}) { - return LocalSubscriptionsCompanion( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon ?? this.icon, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (actorId.present) { - map['actor_id'] = Variable(actorId.value); - } - if (icon.present) { - map['icon'] = Variable(icon.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV1 extends GeneratedDatabase { - DatabaseAtV1(QueryExecutor e) : super(e); - late final Accounts accounts = Accounts(this); - late final Favorites favorites = Favorites(this); - late final LocalSubscriptions localSubscriptions = LocalSubscriptions(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [accounts, favorites, localSubscriptions]; - @override - int get schemaVersion => 1; -} diff --git a/test/drift/thunder/generated/schema_v2.dart b/test/drift/thunder/generated/schema_v2.dart deleted file mode 100644 index 5dca542f4..000000000 --- a/test/drift/thunder/generated/schema_v2.dart +++ /dev/null @@ -1,935 +0,0 @@ -// dart format width=80 -// GENERATED BY drift_dev, DO NOT MODIFY. -// ignore_for_file: type=lint,unused_import -// -import 'package:drift/drift.dart'; - -class Accounts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Accounts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn jwt = GeneratedColumn( - 'jwt', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn instance = GeneratedColumn( - 'instance', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn anonymous = GeneratedColumn( - 'anonymous', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("anonymous" IN (0, 1))'), - defaultValue: const CustomExpression('0')); - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - @override - List get $columns => - [id, username, jwt, instance, anonymous, userId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'accounts'; - @override - Set get $primaryKey => {id}; - @override - AccountsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AccountsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username']), - jwt: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}jwt']), - instance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}instance']), - anonymous: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}anonymous'])!, - userId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}user_id']), - ); - } - - @override - Accounts createAlias(String alias) { - return Accounts(attachedDatabase, alias); - } -} - -class AccountsData extends DataClass implements Insertable { - final int id; - final String? username; - final String? jwt; - final String? instance; - final bool anonymous; - final int? userId; - const AccountsData( - {required this.id, - this.username, - this.jwt, - this.instance, - required this.anonymous, - this.userId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - if (!nullToAbsent || username != null) { - map['username'] = Variable(username); - } - if (!nullToAbsent || jwt != null) { - map['jwt'] = Variable(jwt); - } - if (!nullToAbsent || instance != null) { - map['instance'] = Variable(instance); - } - map['anonymous'] = Variable(anonymous); - if (!nullToAbsent || userId != null) { - map['user_id'] = Variable(userId); - } - return map; - } - - AccountsCompanion toCompanion(bool nullToAbsent) { - return AccountsCompanion( - id: Value(id), - username: username == null && nullToAbsent - ? const Value.absent() - : Value(username), - jwt: jwt == null && nullToAbsent ? const Value.absent() : Value(jwt), - instance: instance == null && nullToAbsent - ? const Value.absent() - : Value(instance), - anonymous: Value(anonymous), - userId: - userId == null && nullToAbsent ? const Value.absent() : Value(userId), - ); - } - - factory AccountsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AccountsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - jwt: serializer.fromJson(json['jwt']), - instance: serializer.fromJson(json['instance']), - anonymous: serializer.fromJson(json['anonymous']), - userId: serializer.fromJson(json['userId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'jwt': serializer.toJson(jwt), - 'instance': serializer.toJson(instance), - 'anonymous': serializer.toJson(anonymous), - 'userId': serializer.toJson(userId), - }; - } - - AccountsData copyWith( - {int? id, - Value username = const Value.absent(), - Value jwt = const Value.absent(), - Value instance = const Value.absent(), - bool? anonymous, - Value userId = const Value.absent()}) => - AccountsData( - id: id ?? this.id, - username: username.present ? username.value : this.username, - jwt: jwt.present ? jwt.value : this.jwt, - instance: instance.present ? instance.value : this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId.present ? userId.value : this.userId, - ); - AccountsData copyWithCompanion(AccountsCompanion data) { - return AccountsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - jwt: data.jwt.present ? data.jwt.value : this.jwt, - instance: data.instance.present ? data.instance.value : this.instance, - anonymous: data.anonymous.present ? data.anonymous.value : this.anonymous, - userId: data.userId.present ? data.userId.value : this.userId, - ); - } - - @override - String toString() { - return (StringBuffer('AccountsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(id, username, jwt, instance, anonymous, userId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AccountsData && - other.id == this.id && - other.username == this.username && - other.jwt == this.jwt && - other.instance == this.instance && - other.anonymous == this.anonymous && - other.userId == this.userId); -} - -class AccountsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value jwt; - final Value instance; - final Value anonymous; - final Value userId; - const AccountsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - }); - AccountsCompanion.insert({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - }); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? jwt, - Expression? instance, - Expression? anonymous, - Expression? userId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (jwt != null) 'jwt': jwt, - if (instance != null) 'instance': instance, - if (anonymous != null) 'anonymous': anonymous, - if (userId != null) 'user_id': userId, - }); - } - - AccountsCompanion copyWith( - {Value? id, - Value? username, - Value? jwt, - Value? instance, - Value? anonymous, - Value? userId}) { - return AccountsCompanion( - id: id ?? this.id, - username: username ?? this.username, - jwt: jwt ?? this.jwt, - instance: instance ?? this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId ?? this.userId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (jwt.present) { - map['jwt'] = Variable(jwt.value); - } - if (instance.present) { - map['instance'] = Variable(instance.value); - } - if (anonymous.present) { - map['anonymous'] = Variable(anonymous.value); - } - if (userId.present) { - map['user_id'] = Variable(userId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AccountsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId') - ..write(')')) - .toString(); - } -} - -class Favorites extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Favorites(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - late final GeneratedColumn communityId = GeneratedColumn( - 'community_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, accountId, communityId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'favorites'; - @override - Set get $primaryKey => {id}; - @override - FavoritesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return FavoritesData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - accountId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}account_id'])!, - communityId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}community_id'])!, - ); - } - - @override - Favorites createAlias(String alias) { - return Favorites(attachedDatabase, alias); - } -} - -class FavoritesData extends DataClass implements Insertable { - final int id; - final int accountId; - final int communityId; - const FavoritesData( - {required this.id, required this.accountId, required this.communityId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['account_id'] = Variable(accountId); - map['community_id'] = Variable(communityId); - return map; - } - - FavoritesCompanion toCompanion(bool nullToAbsent) { - return FavoritesCompanion( - id: Value(id), - accountId: Value(accountId), - communityId: Value(communityId), - ); - } - - factory FavoritesData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return FavoritesData( - id: serializer.fromJson(json['id']), - accountId: serializer.fromJson(json['accountId']), - communityId: serializer.fromJson(json['communityId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'accountId': serializer.toJson(accountId), - 'communityId': serializer.toJson(communityId), - }; - } - - FavoritesData copyWith({int? id, int? accountId, int? communityId}) => - FavoritesData( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - FavoritesData copyWithCompanion(FavoritesCompanion data) { - return FavoritesData( - id: data.id.present ? data.id.value : this.id, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - communityId: - data.communityId.present ? data.communityId.value : this.communityId, - ); - } - - @override - String toString() { - return (StringBuffer('FavoritesData(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, accountId, communityId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is FavoritesData && - other.id == this.id && - other.accountId == this.accountId && - other.communityId == this.communityId); -} - -class FavoritesCompanion extends UpdateCompanion { - final Value id; - final Value accountId; - final Value communityId; - const FavoritesCompanion({ - this.id = const Value.absent(), - this.accountId = const Value.absent(), - this.communityId = const Value.absent(), - }); - FavoritesCompanion.insert({ - this.id = const Value.absent(), - required int accountId, - required int communityId, - }) : accountId = Value(accountId), - communityId = Value(communityId); - static Insertable custom({ - Expression? id, - Expression? accountId, - Expression? communityId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (accountId != null) 'account_id': accountId, - if (communityId != null) 'community_id': communityId, - }); - } - - FavoritesCompanion copyWith( - {Value? id, Value? accountId, Value? communityId}) { - return FavoritesCompanion( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (communityId.present) { - map['community_id'] = Variable(communityId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('FavoritesCompanion(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } -} - -class LocalSubscriptions extends Table - with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - LocalSubscriptions(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn actorId = GeneratedColumn( - 'actor_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn icon = GeneratedColumn( - 'icon', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => [id, name, title, actorId, icon]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'local_subscriptions'; - @override - Set get $primaryKey => {id}; - @override - LocalSubscriptionsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return LocalSubscriptionsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title'])!, - actorId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}actor_id'])!, - icon: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}icon']), - ); - } - - @override - LocalSubscriptions createAlias(String alias) { - return LocalSubscriptions(attachedDatabase, alias); - } -} - -class LocalSubscriptionsData extends DataClass - implements Insertable { - final int id; - final String name; - final String title; - final String actorId; - final String? icon; - const LocalSubscriptionsData( - {required this.id, - required this.name, - required this.title, - required this.actorId, - this.icon}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['title'] = Variable(title); - map['actor_id'] = Variable(actorId); - if (!nullToAbsent || icon != null) { - map['icon'] = Variable(icon); - } - return map; - } - - LocalSubscriptionsCompanion toCompanion(bool nullToAbsent) { - return LocalSubscriptionsCompanion( - id: Value(id), - name: Value(name), - title: Value(title), - actorId: Value(actorId), - icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon), - ); - } - - factory LocalSubscriptionsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return LocalSubscriptionsData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - title: serializer.fromJson(json['title']), - actorId: serializer.fromJson(json['actorId']), - icon: serializer.fromJson(json['icon']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'title': serializer.toJson(title), - 'actorId': serializer.toJson(actorId), - 'icon': serializer.toJson(icon), - }; - } - - LocalSubscriptionsData copyWith( - {int? id, - String? name, - String? title, - String? actorId, - Value icon = const Value.absent()}) => - LocalSubscriptionsData( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon.present ? icon.value : this.icon, - ); - LocalSubscriptionsData copyWithCompanion(LocalSubscriptionsCompanion data) { - return LocalSubscriptionsData( - id: data.id.present ? data.id.value : this.id, - name: data.name.present ? data.name.value : this.name, - title: data.title.present ? data.title.value : this.title, - actorId: data.actorId.present ? data.actorId.value : this.actorId, - icon: data.icon.present ? data.icon.value : this.icon, - ); - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, title, actorId, icon); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is LocalSubscriptionsData && - other.id == this.id && - other.name == this.name && - other.title == this.title && - other.actorId == this.actorId && - other.icon == this.icon); -} - -class LocalSubscriptionsCompanion - extends UpdateCompanion { - final Value id; - final Value name; - final Value title; - final Value actorId; - final Value icon; - const LocalSubscriptionsCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.title = const Value.absent(), - this.actorId = const Value.absent(), - this.icon = const Value.absent(), - }); - LocalSubscriptionsCompanion.insert({ - this.id = const Value.absent(), - required String name, - required String title, - required String actorId, - this.icon = const Value.absent(), - }) : name = Value(name), - title = Value(title), - actorId = Value(actorId); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? title, - Expression? actorId, - Expression? icon, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (title != null) 'title': title, - if (actorId != null) 'actor_id': actorId, - if (icon != null) 'icon': icon, - }); - } - - LocalSubscriptionsCompanion copyWith( - {Value? id, - Value? name, - Value? title, - Value? actorId, - Value? icon}) { - return LocalSubscriptionsCompanion( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon ?? this.icon, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (actorId.present) { - map['actor_id'] = Variable(actorId.value); - } - if (icon.present) { - map['icon'] = Variable(icon.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } -} - -class UserLabels extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - UserLabels(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn label = GeneratedColumn( - 'label', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - @override - List get $columns => [id, username, label]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'user_labels'; - @override - Set get $primaryKey => {id}; - @override - UserLabelsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return UserLabelsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username'])!, - label: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}label'])!, - ); - } - - @override - UserLabels createAlias(String alias) { - return UserLabels(attachedDatabase, alias); - } -} - -class UserLabelsData extends DataClass implements Insertable { - final int id; - final String username; - final String label; - const UserLabelsData( - {required this.id, required this.username, required this.label}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['username'] = Variable(username); - map['label'] = Variable(label); - return map; - } - - UserLabelsCompanion toCompanion(bool nullToAbsent) { - return UserLabelsCompanion( - id: Value(id), - username: Value(username), - label: Value(label), - ); - } - - factory UserLabelsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return UserLabelsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - label: serializer.fromJson(json['label']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'label': serializer.toJson(label), - }; - } - - UserLabelsData copyWith({int? id, String? username, String? label}) => - UserLabelsData( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - UserLabelsData copyWithCompanion(UserLabelsCompanion data) { - return UserLabelsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - label: data.label.present ? data.label.value : this.label, - ); - } - - @override - String toString() { - return (StringBuffer('UserLabelsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, username, label); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is UserLabelsData && - other.id == this.id && - other.username == this.username && - other.label == this.label); -} - -class UserLabelsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value label; - const UserLabelsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.label = const Value.absent(), - }); - UserLabelsCompanion.insert({ - this.id = const Value.absent(), - required String username, - required String label, - }) : username = Value(username), - label = Value(label); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? label, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (label != null) 'label': label, - }); - } - - UserLabelsCompanion copyWith( - {Value? id, Value? username, Value? label}) { - return UserLabelsCompanion( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (label.present) { - map['label'] = Variable(label.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('UserLabelsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV2 extends GeneratedDatabase { - DatabaseAtV2(QueryExecutor e) : super(e); - late final Accounts accounts = Accounts(this); - late final Favorites favorites = Favorites(this); - late final LocalSubscriptions localSubscriptions = LocalSubscriptions(this); - late final UserLabels userLabels = UserLabels(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [accounts, favorites, localSubscriptions, userLabels]; - @override - int get schemaVersion => 2; -} diff --git a/test/drift/thunder/generated/schema_v3.dart b/test/drift/thunder/generated/schema_v3.dart deleted file mode 100644 index 5add1778e..000000000 --- a/test/drift/thunder/generated/schema_v3.dart +++ /dev/null @@ -1,1253 +0,0 @@ -// dart format width=80 -// GENERATED BY drift_dev, DO NOT MODIFY. -// ignore_for_file: type=lint,unused_import -// -import 'package:drift/drift.dart'; - -class Accounts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Accounts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn jwt = GeneratedColumn( - 'jwt', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn instance = GeneratedColumn( - 'instance', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn anonymous = GeneratedColumn( - 'anonymous', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("anonymous" IN (0, 1))'), - defaultValue: const CustomExpression('0')); - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - @override - List get $columns => - [id, username, jwt, instance, anonymous, userId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'accounts'; - @override - Set get $primaryKey => {id}; - @override - AccountsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AccountsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username']), - jwt: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}jwt']), - instance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}instance']), - anonymous: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}anonymous'])!, - userId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}user_id']), - ); - } - - @override - Accounts createAlias(String alias) { - return Accounts(attachedDatabase, alias); - } -} - -class AccountsData extends DataClass implements Insertable { - final int id; - final String? username; - final String? jwt; - final String? instance; - final bool anonymous; - final int? userId; - const AccountsData( - {required this.id, - this.username, - this.jwt, - this.instance, - required this.anonymous, - this.userId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - if (!nullToAbsent || username != null) { - map['username'] = Variable(username); - } - if (!nullToAbsent || jwt != null) { - map['jwt'] = Variable(jwt); - } - if (!nullToAbsent || instance != null) { - map['instance'] = Variable(instance); - } - map['anonymous'] = Variable(anonymous); - if (!nullToAbsent || userId != null) { - map['user_id'] = Variable(userId); - } - return map; - } - - AccountsCompanion toCompanion(bool nullToAbsent) { - return AccountsCompanion( - id: Value(id), - username: username == null && nullToAbsent - ? const Value.absent() - : Value(username), - jwt: jwt == null && nullToAbsent ? const Value.absent() : Value(jwt), - instance: instance == null && nullToAbsent - ? const Value.absent() - : Value(instance), - anonymous: Value(anonymous), - userId: - userId == null && nullToAbsent ? const Value.absent() : Value(userId), - ); - } - - factory AccountsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AccountsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - jwt: serializer.fromJson(json['jwt']), - instance: serializer.fromJson(json['instance']), - anonymous: serializer.fromJson(json['anonymous']), - userId: serializer.fromJson(json['userId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'jwt': serializer.toJson(jwt), - 'instance': serializer.toJson(instance), - 'anonymous': serializer.toJson(anonymous), - 'userId': serializer.toJson(userId), - }; - } - - AccountsData copyWith( - {int? id, - Value username = const Value.absent(), - Value jwt = const Value.absent(), - Value instance = const Value.absent(), - bool? anonymous, - Value userId = const Value.absent()}) => - AccountsData( - id: id ?? this.id, - username: username.present ? username.value : this.username, - jwt: jwt.present ? jwt.value : this.jwt, - instance: instance.present ? instance.value : this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId.present ? userId.value : this.userId, - ); - AccountsData copyWithCompanion(AccountsCompanion data) { - return AccountsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - jwt: data.jwt.present ? data.jwt.value : this.jwt, - instance: data.instance.present ? data.instance.value : this.instance, - anonymous: data.anonymous.present ? data.anonymous.value : this.anonymous, - userId: data.userId.present ? data.userId.value : this.userId, - ); - } - - @override - String toString() { - return (StringBuffer('AccountsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(id, username, jwt, instance, anonymous, userId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AccountsData && - other.id == this.id && - other.username == this.username && - other.jwt == this.jwt && - other.instance == this.instance && - other.anonymous == this.anonymous && - other.userId == this.userId); -} - -class AccountsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value jwt; - final Value instance; - final Value anonymous; - final Value userId; - const AccountsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - }); - AccountsCompanion.insert({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - }); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? jwt, - Expression? instance, - Expression? anonymous, - Expression? userId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (jwt != null) 'jwt': jwt, - if (instance != null) 'instance': instance, - if (anonymous != null) 'anonymous': anonymous, - if (userId != null) 'user_id': userId, - }); - } - - AccountsCompanion copyWith( - {Value? id, - Value? username, - Value? jwt, - Value? instance, - Value? anonymous, - Value? userId}) { - return AccountsCompanion( - id: id ?? this.id, - username: username ?? this.username, - jwt: jwt ?? this.jwt, - instance: instance ?? this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId ?? this.userId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (jwt.present) { - map['jwt'] = Variable(jwt.value); - } - if (instance.present) { - map['instance'] = Variable(instance.value); - } - if (anonymous.present) { - map['anonymous'] = Variable(anonymous.value); - } - if (userId.present) { - map['user_id'] = Variable(userId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AccountsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId') - ..write(')')) - .toString(); - } -} - -class Favorites extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Favorites(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - late final GeneratedColumn communityId = GeneratedColumn( - 'community_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, accountId, communityId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'favorites'; - @override - Set get $primaryKey => {id}; - @override - FavoritesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return FavoritesData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - accountId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}account_id'])!, - communityId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}community_id'])!, - ); - } - - @override - Favorites createAlias(String alias) { - return Favorites(attachedDatabase, alias); - } -} - -class FavoritesData extends DataClass implements Insertable { - final int id; - final int accountId; - final int communityId; - const FavoritesData( - {required this.id, required this.accountId, required this.communityId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['account_id'] = Variable(accountId); - map['community_id'] = Variable(communityId); - return map; - } - - FavoritesCompanion toCompanion(bool nullToAbsent) { - return FavoritesCompanion( - id: Value(id), - accountId: Value(accountId), - communityId: Value(communityId), - ); - } - - factory FavoritesData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return FavoritesData( - id: serializer.fromJson(json['id']), - accountId: serializer.fromJson(json['accountId']), - communityId: serializer.fromJson(json['communityId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'accountId': serializer.toJson(accountId), - 'communityId': serializer.toJson(communityId), - }; - } - - FavoritesData copyWith({int? id, int? accountId, int? communityId}) => - FavoritesData( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - FavoritesData copyWithCompanion(FavoritesCompanion data) { - return FavoritesData( - id: data.id.present ? data.id.value : this.id, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - communityId: - data.communityId.present ? data.communityId.value : this.communityId, - ); - } - - @override - String toString() { - return (StringBuffer('FavoritesData(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, accountId, communityId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is FavoritesData && - other.id == this.id && - other.accountId == this.accountId && - other.communityId == this.communityId); -} - -class FavoritesCompanion extends UpdateCompanion { - final Value id; - final Value accountId; - final Value communityId; - const FavoritesCompanion({ - this.id = const Value.absent(), - this.accountId = const Value.absent(), - this.communityId = const Value.absent(), - }); - FavoritesCompanion.insert({ - this.id = const Value.absent(), - required int accountId, - required int communityId, - }) : accountId = Value(accountId), - communityId = Value(communityId); - static Insertable custom({ - Expression? id, - Expression? accountId, - Expression? communityId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (accountId != null) 'account_id': accountId, - if (communityId != null) 'community_id': communityId, - }); - } - - FavoritesCompanion copyWith( - {Value? id, Value? accountId, Value? communityId}) { - return FavoritesCompanion( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (communityId.present) { - map['community_id'] = Variable(communityId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('FavoritesCompanion(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } -} - -class LocalSubscriptions extends Table - with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - LocalSubscriptions(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn actorId = GeneratedColumn( - 'actor_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn icon = GeneratedColumn( - 'icon', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => [id, name, title, actorId, icon]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'local_subscriptions'; - @override - Set get $primaryKey => {id}; - @override - LocalSubscriptionsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return LocalSubscriptionsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title'])!, - actorId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}actor_id'])!, - icon: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}icon']), - ); - } - - @override - LocalSubscriptions createAlias(String alias) { - return LocalSubscriptions(attachedDatabase, alias); - } -} - -class LocalSubscriptionsData extends DataClass - implements Insertable { - final int id; - final String name; - final String title; - final String actorId; - final String? icon; - const LocalSubscriptionsData( - {required this.id, - required this.name, - required this.title, - required this.actorId, - this.icon}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['title'] = Variable(title); - map['actor_id'] = Variable(actorId); - if (!nullToAbsent || icon != null) { - map['icon'] = Variable(icon); - } - return map; - } - - LocalSubscriptionsCompanion toCompanion(bool nullToAbsent) { - return LocalSubscriptionsCompanion( - id: Value(id), - name: Value(name), - title: Value(title), - actorId: Value(actorId), - icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon), - ); - } - - factory LocalSubscriptionsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return LocalSubscriptionsData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - title: serializer.fromJson(json['title']), - actorId: serializer.fromJson(json['actorId']), - icon: serializer.fromJson(json['icon']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'title': serializer.toJson(title), - 'actorId': serializer.toJson(actorId), - 'icon': serializer.toJson(icon), - }; - } - - LocalSubscriptionsData copyWith( - {int? id, - String? name, - String? title, - String? actorId, - Value icon = const Value.absent()}) => - LocalSubscriptionsData( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon.present ? icon.value : this.icon, - ); - LocalSubscriptionsData copyWithCompanion(LocalSubscriptionsCompanion data) { - return LocalSubscriptionsData( - id: data.id.present ? data.id.value : this.id, - name: data.name.present ? data.name.value : this.name, - title: data.title.present ? data.title.value : this.title, - actorId: data.actorId.present ? data.actorId.value : this.actorId, - icon: data.icon.present ? data.icon.value : this.icon, - ); - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, title, actorId, icon); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is LocalSubscriptionsData && - other.id == this.id && - other.name == this.name && - other.title == this.title && - other.actorId == this.actorId && - other.icon == this.icon); -} - -class LocalSubscriptionsCompanion - extends UpdateCompanion { - final Value id; - final Value name; - final Value title; - final Value actorId; - final Value icon; - const LocalSubscriptionsCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.title = const Value.absent(), - this.actorId = const Value.absent(), - this.icon = const Value.absent(), - }); - LocalSubscriptionsCompanion.insert({ - this.id = const Value.absent(), - required String name, - required String title, - required String actorId, - this.icon = const Value.absent(), - }) : name = Value(name), - title = Value(title), - actorId = Value(actorId); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? title, - Expression? actorId, - Expression? icon, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (title != null) 'title': title, - if (actorId != null) 'actor_id': actorId, - if (icon != null) 'icon': icon, - }); - } - - LocalSubscriptionsCompanion copyWith( - {Value? id, - Value? name, - Value? title, - Value? actorId, - Value? icon}) { - return LocalSubscriptionsCompanion( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon ?? this.icon, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (actorId.present) { - map['actor_id'] = Variable(actorId.value); - } - if (icon.present) { - map['icon'] = Variable(icon.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } -} - -class UserLabels extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - UserLabels(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn label = GeneratedColumn( - 'label', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - @override - List get $columns => [id, username, label]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'user_labels'; - @override - Set get $primaryKey => {id}; - @override - UserLabelsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return UserLabelsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username'])!, - label: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}label'])!, - ); - } - - @override - UserLabels createAlias(String alias) { - return UserLabels(attachedDatabase, alias); - } -} - -class UserLabelsData extends DataClass implements Insertable { - final int id; - final String username; - final String label; - const UserLabelsData( - {required this.id, required this.username, required this.label}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['username'] = Variable(username); - map['label'] = Variable(label); - return map; - } - - UserLabelsCompanion toCompanion(bool nullToAbsent) { - return UserLabelsCompanion( - id: Value(id), - username: Value(username), - label: Value(label), - ); - } - - factory UserLabelsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return UserLabelsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - label: serializer.fromJson(json['label']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'label': serializer.toJson(label), - }; - } - - UserLabelsData copyWith({int? id, String? username, String? label}) => - UserLabelsData( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - UserLabelsData copyWithCompanion(UserLabelsCompanion data) { - return UserLabelsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - label: data.label.present ? data.label.value : this.label, - ); - } - - @override - String toString() { - return (StringBuffer('UserLabelsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, username, label); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is UserLabelsData && - other.id == this.id && - other.username == this.username && - other.label == this.label); -} - -class UserLabelsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value label; - const UserLabelsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.label = const Value.absent(), - }); - UserLabelsCompanion.insert({ - this.id = const Value.absent(), - required String username, - required String label, - }) : username = Value(username), - label = Value(label); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? label, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (label != null) 'label': label, - }); - } - - UserLabelsCompanion copyWith( - {Value? id, Value? username, Value? label}) { - return UserLabelsCompanion( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (label.present) { - map['label'] = Variable(label.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('UserLabelsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } -} - -class Drafts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Drafts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn draftType = GeneratedColumn( - 'draft_type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn existingId = GeneratedColumn( - 'existing_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn replyId = GeneratedColumn( - 'reply_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn url = GeneratedColumn( - 'url', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn body = GeneratedColumn( - 'body', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => - [id, draftType, existingId, replyId, title, url, body]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'drafts'; - @override - Set get $primaryKey => {id}; - @override - DraftsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return DraftsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - draftType: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}draft_type'])!, - existingId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}existing_id']), - replyId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}reply_id']), - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title']), - url: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}url']), - body: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}body']), - ); - } - - @override - Drafts createAlias(String alias) { - return Drafts(attachedDatabase, alias); - } -} - -class DraftsData extends DataClass implements Insertable { - final int id; - final String draftType; - final int? existingId; - final int? replyId; - final String? title; - final String? url; - final String? body; - const DraftsData( - {required this.id, - required this.draftType, - this.existingId, - this.replyId, - this.title, - this.url, - this.body}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['draft_type'] = Variable(draftType); - if (!nullToAbsent || existingId != null) { - map['existing_id'] = Variable(existingId); - } - if (!nullToAbsent || replyId != null) { - map['reply_id'] = Variable(replyId); - } - if (!nullToAbsent || title != null) { - map['title'] = Variable(title); - } - if (!nullToAbsent || url != null) { - map['url'] = Variable(url); - } - if (!nullToAbsent || body != null) { - map['body'] = Variable(body); - } - return map; - } - - DraftsCompanion toCompanion(bool nullToAbsent) { - return DraftsCompanion( - id: Value(id), - draftType: Value(draftType), - existingId: existingId == null && nullToAbsent - ? const Value.absent() - : Value(existingId), - replyId: replyId == null && nullToAbsent - ? const Value.absent() - : Value(replyId), - title: - title == null && nullToAbsent ? const Value.absent() : Value(title), - url: url == null && nullToAbsent ? const Value.absent() : Value(url), - body: body == null && nullToAbsent ? const Value.absent() : Value(body), - ); - } - - factory DraftsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return DraftsData( - id: serializer.fromJson(json['id']), - draftType: serializer.fromJson(json['draftType']), - existingId: serializer.fromJson(json['existingId']), - replyId: serializer.fromJson(json['replyId']), - title: serializer.fromJson(json['title']), - url: serializer.fromJson(json['url']), - body: serializer.fromJson(json['body']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'draftType': serializer.toJson(draftType), - 'existingId': serializer.toJson(existingId), - 'replyId': serializer.toJson(replyId), - 'title': serializer.toJson(title), - 'url': serializer.toJson(url), - 'body': serializer.toJson(body), - }; - } - - DraftsData copyWith( - {int? id, - String? draftType, - Value existingId = const Value.absent(), - Value replyId = const Value.absent(), - Value title = const Value.absent(), - Value url = const Value.absent(), - Value body = const Value.absent()}) => - DraftsData( - id: id ?? this.id, - draftType: draftType ?? this.draftType, - existingId: existingId.present ? existingId.value : this.existingId, - replyId: replyId.present ? replyId.value : this.replyId, - title: title.present ? title.value : this.title, - url: url.present ? url.value : this.url, - body: body.present ? body.value : this.body, - ); - DraftsData copyWithCompanion(DraftsCompanion data) { - return DraftsData( - id: data.id.present ? data.id.value : this.id, - draftType: data.draftType.present ? data.draftType.value : this.draftType, - existingId: - data.existingId.present ? data.existingId.value : this.existingId, - replyId: data.replyId.present ? data.replyId.value : this.replyId, - title: data.title.present ? data.title.value : this.title, - url: data.url.present ? data.url.value : this.url, - body: data.body.present ? data.body.value : this.body, - ); - } - - @override - String toString() { - return (StringBuffer('DraftsData(') - ..write('id: $id, ') - ..write('draftType: $draftType, ') - ..write('existingId: $existingId, ') - ..write('replyId: $replyId, ') - ..write('title: $title, ') - ..write('url: $url, ') - ..write('body: $body') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(id, draftType, existingId, replyId, title, url, body); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is DraftsData && - other.id == this.id && - other.draftType == this.draftType && - other.existingId == this.existingId && - other.replyId == this.replyId && - other.title == this.title && - other.url == this.url && - other.body == this.body); -} - -class DraftsCompanion extends UpdateCompanion { - final Value id; - final Value draftType; - final Value existingId; - final Value replyId; - final Value title; - final Value url; - final Value body; - const DraftsCompanion({ - this.id = const Value.absent(), - this.draftType = const Value.absent(), - this.existingId = const Value.absent(), - this.replyId = const Value.absent(), - this.title = const Value.absent(), - this.url = const Value.absent(), - this.body = const Value.absent(), - }); - DraftsCompanion.insert({ - this.id = const Value.absent(), - required String draftType, - this.existingId = const Value.absent(), - this.replyId = const Value.absent(), - this.title = const Value.absent(), - this.url = const Value.absent(), - this.body = const Value.absent(), - }) : draftType = Value(draftType); - static Insertable custom({ - Expression? id, - Expression? draftType, - Expression? existingId, - Expression? replyId, - Expression? title, - Expression? url, - Expression? body, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (draftType != null) 'draft_type': draftType, - if (existingId != null) 'existing_id': existingId, - if (replyId != null) 'reply_id': replyId, - if (title != null) 'title': title, - if (url != null) 'url': url, - if (body != null) 'body': body, - }); - } - - DraftsCompanion copyWith( - {Value? id, - Value? draftType, - Value? existingId, - Value? replyId, - Value? title, - Value? url, - Value? body}) { - return DraftsCompanion( - id: id ?? this.id, - draftType: draftType ?? this.draftType, - existingId: existingId ?? this.existingId, - replyId: replyId ?? this.replyId, - title: title ?? this.title, - url: url ?? this.url, - body: body ?? this.body, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (draftType.present) { - map['draft_type'] = Variable(draftType.value); - } - if (existingId.present) { - map['existing_id'] = Variable(existingId.value); - } - if (replyId.present) { - map['reply_id'] = Variable(replyId.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (url.present) { - map['url'] = Variable(url.value); - } - if (body.present) { - map['body'] = Variable(body.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('DraftsCompanion(') - ..write('id: $id, ') - ..write('draftType: $draftType, ') - ..write('existingId: $existingId, ') - ..write('replyId: $replyId, ') - ..write('title: $title, ') - ..write('url: $url, ') - ..write('body: $body') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV3 extends GeneratedDatabase { - DatabaseAtV3(QueryExecutor e) : super(e); - late final Accounts accounts = Accounts(this); - late final Favorites favorites = Favorites(this); - late final LocalSubscriptions localSubscriptions = LocalSubscriptions(this); - late final UserLabels userLabels = UserLabels(this); - late final Drafts drafts = Drafts(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [accounts, favorites, localSubscriptions, userLabels, drafts]; - @override - int get schemaVersion => 3; -} diff --git a/test/drift/thunder/generated/schema_v4.dart b/test/drift/thunder/generated/schema_v4.dart deleted file mode 100644 index 22ba7b1e2..000000000 --- a/test/drift/thunder/generated/schema_v4.dart +++ /dev/null @@ -1,1288 +0,0 @@ -// dart format width=80 -// GENERATED BY drift_dev, DO NOT MODIFY. -// ignore_for_file: type=lint,unused_import -// -import 'package:drift/drift.dart'; - -class Accounts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Accounts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn jwt = GeneratedColumn( - 'jwt', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn instance = GeneratedColumn( - 'instance', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn anonymous = GeneratedColumn( - 'anonymous', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("anonymous" IN (0, 1))'), - defaultValue: const CustomExpression('0')); - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - @override - List get $columns => - [id, username, jwt, instance, anonymous, userId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'accounts'; - @override - Set get $primaryKey => {id}; - @override - AccountsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AccountsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username']), - jwt: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}jwt']), - instance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}instance']), - anonymous: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}anonymous'])!, - userId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}user_id']), - ); - } - - @override - Accounts createAlias(String alias) { - return Accounts(attachedDatabase, alias); - } -} - -class AccountsData extends DataClass implements Insertable { - final int id; - final String? username; - final String? jwt; - final String? instance; - final bool anonymous; - final int? userId; - const AccountsData( - {required this.id, - this.username, - this.jwt, - this.instance, - required this.anonymous, - this.userId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - if (!nullToAbsent || username != null) { - map['username'] = Variable(username); - } - if (!nullToAbsent || jwt != null) { - map['jwt'] = Variable(jwt); - } - if (!nullToAbsent || instance != null) { - map['instance'] = Variable(instance); - } - map['anonymous'] = Variable(anonymous); - if (!nullToAbsent || userId != null) { - map['user_id'] = Variable(userId); - } - return map; - } - - AccountsCompanion toCompanion(bool nullToAbsent) { - return AccountsCompanion( - id: Value(id), - username: username == null && nullToAbsent - ? const Value.absent() - : Value(username), - jwt: jwt == null && nullToAbsent ? const Value.absent() : Value(jwt), - instance: instance == null && nullToAbsent - ? const Value.absent() - : Value(instance), - anonymous: Value(anonymous), - userId: - userId == null && nullToAbsent ? const Value.absent() : Value(userId), - ); - } - - factory AccountsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AccountsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - jwt: serializer.fromJson(json['jwt']), - instance: serializer.fromJson(json['instance']), - anonymous: serializer.fromJson(json['anonymous']), - userId: serializer.fromJson(json['userId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'jwt': serializer.toJson(jwt), - 'instance': serializer.toJson(instance), - 'anonymous': serializer.toJson(anonymous), - 'userId': serializer.toJson(userId), - }; - } - - AccountsData copyWith( - {int? id, - Value username = const Value.absent(), - Value jwt = const Value.absent(), - Value instance = const Value.absent(), - bool? anonymous, - Value userId = const Value.absent()}) => - AccountsData( - id: id ?? this.id, - username: username.present ? username.value : this.username, - jwt: jwt.present ? jwt.value : this.jwt, - instance: instance.present ? instance.value : this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId.present ? userId.value : this.userId, - ); - AccountsData copyWithCompanion(AccountsCompanion data) { - return AccountsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - jwt: data.jwt.present ? data.jwt.value : this.jwt, - instance: data.instance.present ? data.instance.value : this.instance, - anonymous: data.anonymous.present ? data.anonymous.value : this.anonymous, - userId: data.userId.present ? data.userId.value : this.userId, - ); - } - - @override - String toString() { - return (StringBuffer('AccountsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(id, username, jwt, instance, anonymous, userId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AccountsData && - other.id == this.id && - other.username == this.username && - other.jwt == this.jwt && - other.instance == this.instance && - other.anonymous == this.anonymous && - other.userId == this.userId); -} - -class AccountsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value jwt; - final Value instance; - final Value anonymous; - final Value userId; - const AccountsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - }); - AccountsCompanion.insert({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - }); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? jwt, - Expression? instance, - Expression? anonymous, - Expression? userId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (jwt != null) 'jwt': jwt, - if (instance != null) 'instance': instance, - if (anonymous != null) 'anonymous': anonymous, - if (userId != null) 'user_id': userId, - }); - } - - AccountsCompanion copyWith( - {Value? id, - Value? username, - Value? jwt, - Value? instance, - Value? anonymous, - Value? userId}) { - return AccountsCompanion( - id: id ?? this.id, - username: username ?? this.username, - jwt: jwt ?? this.jwt, - instance: instance ?? this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId ?? this.userId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (jwt.present) { - map['jwt'] = Variable(jwt.value); - } - if (instance.present) { - map['instance'] = Variable(instance.value); - } - if (anonymous.present) { - map['anonymous'] = Variable(anonymous.value); - } - if (userId.present) { - map['user_id'] = Variable(userId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AccountsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId') - ..write(')')) - .toString(); - } -} - -class Favorites extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Favorites(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - late final GeneratedColumn communityId = GeneratedColumn( - 'community_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, accountId, communityId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'favorites'; - @override - Set get $primaryKey => {id}; - @override - FavoritesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return FavoritesData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - accountId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}account_id'])!, - communityId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}community_id'])!, - ); - } - - @override - Favorites createAlias(String alias) { - return Favorites(attachedDatabase, alias); - } -} - -class FavoritesData extends DataClass implements Insertable { - final int id; - final int accountId; - final int communityId; - const FavoritesData( - {required this.id, required this.accountId, required this.communityId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['account_id'] = Variable(accountId); - map['community_id'] = Variable(communityId); - return map; - } - - FavoritesCompanion toCompanion(bool nullToAbsent) { - return FavoritesCompanion( - id: Value(id), - accountId: Value(accountId), - communityId: Value(communityId), - ); - } - - factory FavoritesData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return FavoritesData( - id: serializer.fromJson(json['id']), - accountId: serializer.fromJson(json['accountId']), - communityId: serializer.fromJson(json['communityId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'accountId': serializer.toJson(accountId), - 'communityId': serializer.toJson(communityId), - }; - } - - FavoritesData copyWith({int? id, int? accountId, int? communityId}) => - FavoritesData( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - FavoritesData copyWithCompanion(FavoritesCompanion data) { - return FavoritesData( - id: data.id.present ? data.id.value : this.id, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - communityId: - data.communityId.present ? data.communityId.value : this.communityId, - ); - } - - @override - String toString() { - return (StringBuffer('FavoritesData(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, accountId, communityId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is FavoritesData && - other.id == this.id && - other.accountId == this.accountId && - other.communityId == this.communityId); -} - -class FavoritesCompanion extends UpdateCompanion { - final Value id; - final Value accountId; - final Value communityId; - const FavoritesCompanion({ - this.id = const Value.absent(), - this.accountId = const Value.absent(), - this.communityId = const Value.absent(), - }); - FavoritesCompanion.insert({ - this.id = const Value.absent(), - required int accountId, - required int communityId, - }) : accountId = Value(accountId), - communityId = Value(communityId); - static Insertable custom({ - Expression? id, - Expression? accountId, - Expression? communityId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (accountId != null) 'account_id': accountId, - if (communityId != null) 'community_id': communityId, - }); - } - - FavoritesCompanion copyWith( - {Value? id, Value? accountId, Value? communityId}) { - return FavoritesCompanion( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (communityId.present) { - map['community_id'] = Variable(communityId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('FavoritesCompanion(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } -} - -class LocalSubscriptions extends Table - with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - LocalSubscriptions(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn actorId = GeneratedColumn( - 'actor_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn icon = GeneratedColumn( - 'icon', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => [id, name, title, actorId, icon]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'local_subscriptions'; - @override - Set get $primaryKey => {id}; - @override - LocalSubscriptionsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return LocalSubscriptionsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title'])!, - actorId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}actor_id'])!, - icon: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}icon']), - ); - } - - @override - LocalSubscriptions createAlias(String alias) { - return LocalSubscriptions(attachedDatabase, alias); - } -} - -class LocalSubscriptionsData extends DataClass - implements Insertable { - final int id; - final String name; - final String title; - final String actorId; - final String? icon; - const LocalSubscriptionsData( - {required this.id, - required this.name, - required this.title, - required this.actorId, - this.icon}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['title'] = Variable(title); - map['actor_id'] = Variable(actorId); - if (!nullToAbsent || icon != null) { - map['icon'] = Variable(icon); - } - return map; - } - - LocalSubscriptionsCompanion toCompanion(bool nullToAbsent) { - return LocalSubscriptionsCompanion( - id: Value(id), - name: Value(name), - title: Value(title), - actorId: Value(actorId), - icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon), - ); - } - - factory LocalSubscriptionsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return LocalSubscriptionsData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - title: serializer.fromJson(json['title']), - actorId: serializer.fromJson(json['actorId']), - icon: serializer.fromJson(json['icon']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'title': serializer.toJson(title), - 'actorId': serializer.toJson(actorId), - 'icon': serializer.toJson(icon), - }; - } - - LocalSubscriptionsData copyWith( - {int? id, - String? name, - String? title, - String? actorId, - Value icon = const Value.absent()}) => - LocalSubscriptionsData( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon.present ? icon.value : this.icon, - ); - LocalSubscriptionsData copyWithCompanion(LocalSubscriptionsCompanion data) { - return LocalSubscriptionsData( - id: data.id.present ? data.id.value : this.id, - name: data.name.present ? data.name.value : this.name, - title: data.title.present ? data.title.value : this.title, - actorId: data.actorId.present ? data.actorId.value : this.actorId, - icon: data.icon.present ? data.icon.value : this.icon, - ); - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, title, actorId, icon); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is LocalSubscriptionsData && - other.id == this.id && - other.name == this.name && - other.title == this.title && - other.actorId == this.actorId && - other.icon == this.icon); -} - -class LocalSubscriptionsCompanion - extends UpdateCompanion { - final Value id; - final Value name; - final Value title; - final Value actorId; - final Value icon; - const LocalSubscriptionsCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.title = const Value.absent(), - this.actorId = const Value.absent(), - this.icon = const Value.absent(), - }); - LocalSubscriptionsCompanion.insert({ - this.id = const Value.absent(), - required String name, - required String title, - required String actorId, - this.icon = const Value.absent(), - }) : name = Value(name), - title = Value(title), - actorId = Value(actorId); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? title, - Expression? actorId, - Expression? icon, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (title != null) 'title': title, - if (actorId != null) 'actor_id': actorId, - if (icon != null) 'icon': icon, - }); - } - - LocalSubscriptionsCompanion copyWith( - {Value? id, - Value? name, - Value? title, - Value? actorId, - Value? icon}) { - return LocalSubscriptionsCompanion( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon ?? this.icon, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (actorId.present) { - map['actor_id'] = Variable(actorId.value); - } - if (icon.present) { - map['icon'] = Variable(icon.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } -} - -class UserLabels extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - UserLabels(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn label = GeneratedColumn( - 'label', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - @override - List get $columns => [id, username, label]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'user_labels'; - @override - Set get $primaryKey => {id}; - @override - UserLabelsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return UserLabelsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username'])!, - label: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}label'])!, - ); - } - - @override - UserLabels createAlias(String alias) { - return UserLabels(attachedDatabase, alias); - } -} - -class UserLabelsData extends DataClass implements Insertable { - final int id; - final String username; - final String label; - const UserLabelsData( - {required this.id, required this.username, required this.label}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['username'] = Variable(username); - map['label'] = Variable(label); - return map; - } - - UserLabelsCompanion toCompanion(bool nullToAbsent) { - return UserLabelsCompanion( - id: Value(id), - username: Value(username), - label: Value(label), - ); - } - - factory UserLabelsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return UserLabelsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - label: serializer.fromJson(json['label']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'label': serializer.toJson(label), - }; - } - - UserLabelsData copyWith({int? id, String? username, String? label}) => - UserLabelsData( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - UserLabelsData copyWithCompanion(UserLabelsCompanion data) { - return UserLabelsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - label: data.label.present ? data.label.value : this.label, - ); - } - - @override - String toString() { - return (StringBuffer('UserLabelsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, username, label); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is UserLabelsData && - other.id == this.id && - other.username == this.username && - other.label == this.label); -} - -class UserLabelsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value label; - const UserLabelsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.label = const Value.absent(), - }); - UserLabelsCompanion.insert({ - this.id = const Value.absent(), - required String username, - required String label, - }) : username = Value(username), - label = Value(label); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? label, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (label != null) 'label': label, - }); - } - - UserLabelsCompanion copyWith( - {Value? id, Value? username, Value? label}) { - return UserLabelsCompanion( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (label.present) { - map['label'] = Variable(label.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('UserLabelsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } -} - -class Drafts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Drafts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn draftType = GeneratedColumn( - 'draft_type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn existingId = GeneratedColumn( - 'existing_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn replyId = GeneratedColumn( - 'reply_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn url = GeneratedColumn( - 'url', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn customThumbnail = GeneratedColumn( - 'custom_thumbnail', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn body = GeneratedColumn( - 'body', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => - [id, draftType, existingId, replyId, title, url, customThumbnail, body]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'drafts'; - @override - Set get $primaryKey => {id}; - @override - DraftsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return DraftsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - draftType: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}draft_type'])!, - existingId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}existing_id']), - replyId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}reply_id']), - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title']), - url: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}url']), - customThumbnail: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}custom_thumbnail']), - body: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}body']), - ); - } - - @override - Drafts createAlias(String alias) { - return Drafts(attachedDatabase, alias); - } -} - -class DraftsData extends DataClass implements Insertable { - final int id; - final String draftType; - final int? existingId; - final int? replyId; - final String? title; - final String? url; - final String? customThumbnail; - final String? body; - const DraftsData( - {required this.id, - required this.draftType, - this.existingId, - this.replyId, - this.title, - this.url, - this.customThumbnail, - this.body}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['draft_type'] = Variable(draftType); - if (!nullToAbsent || existingId != null) { - map['existing_id'] = Variable(existingId); - } - if (!nullToAbsent || replyId != null) { - map['reply_id'] = Variable(replyId); - } - if (!nullToAbsent || title != null) { - map['title'] = Variable(title); - } - if (!nullToAbsent || url != null) { - map['url'] = Variable(url); - } - if (!nullToAbsent || customThumbnail != null) { - map['custom_thumbnail'] = Variable(customThumbnail); - } - if (!nullToAbsent || body != null) { - map['body'] = Variable(body); - } - return map; - } - - DraftsCompanion toCompanion(bool nullToAbsent) { - return DraftsCompanion( - id: Value(id), - draftType: Value(draftType), - existingId: existingId == null && nullToAbsent - ? const Value.absent() - : Value(existingId), - replyId: replyId == null && nullToAbsent - ? const Value.absent() - : Value(replyId), - title: - title == null && nullToAbsent ? const Value.absent() : Value(title), - url: url == null && nullToAbsent ? const Value.absent() : Value(url), - customThumbnail: customThumbnail == null && nullToAbsent - ? const Value.absent() - : Value(customThumbnail), - body: body == null && nullToAbsent ? const Value.absent() : Value(body), - ); - } - - factory DraftsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return DraftsData( - id: serializer.fromJson(json['id']), - draftType: serializer.fromJson(json['draftType']), - existingId: serializer.fromJson(json['existingId']), - replyId: serializer.fromJson(json['replyId']), - title: serializer.fromJson(json['title']), - url: serializer.fromJson(json['url']), - customThumbnail: serializer.fromJson(json['customThumbnail']), - body: serializer.fromJson(json['body']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'draftType': serializer.toJson(draftType), - 'existingId': serializer.toJson(existingId), - 'replyId': serializer.toJson(replyId), - 'title': serializer.toJson(title), - 'url': serializer.toJson(url), - 'customThumbnail': serializer.toJson(customThumbnail), - 'body': serializer.toJson(body), - }; - } - - DraftsData copyWith( - {int? id, - String? draftType, - Value existingId = const Value.absent(), - Value replyId = const Value.absent(), - Value title = const Value.absent(), - Value url = const Value.absent(), - Value customThumbnail = const Value.absent(), - Value body = const Value.absent()}) => - DraftsData( - id: id ?? this.id, - draftType: draftType ?? this.draftType, - existingId: existingId.present ? existingId.value : this.existingId, - replyId: replyId.present ? replyId.value : this.replyId, - title: title.present ? title.value : this.title, - url: url.present ? url.value : this.url, - customThumbnail: customThumbnail.present - ? customThumbnail.value - : this.customThumbnail, - body: body.present ? body.value : this.body, - ); - DraftsData copyWithCompanion(DraftsCompanion data) { - return DraftsData( - id: data.id.present ? data.id.value : this.id, - draftType: data.draftType.present ? data.draftType.value : this.draftType, - existingId: - data.existingId.present ? data.existingId.value : this.existingId, - replyId: data.replyId.present ? data.replyId.value : this.replyId, - title: data.title.present ? data.title.value : this.title, - url: data.url.present ? data.url.value : this.url, - customThumbnail: data.customThumbnail.present - ? data.customThumbnail.value - : this.customThumbnail, - body: data.body.present ? data.body.value : this.body, - ); - } - - @override - String toString() { - return (StringBuffer('DraftsData(') - ..write('id: $id, ') - ..write('draftType: $draftType, ') - ..write('existingId: $existingId, ') - ..write('replyId: $replyId, ') - ..write('title: $title, ') - ..write('url: $url, ') - ..write('customThumbnail: $customThumbnail, ') - ..write('body: $body') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash( - id, draftType, existingId, replyId, title, url, customThumbnail, body); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is DraftsData && - other.id == this.id && - other.draftType == this.draftType && - other.existingId == this.existingId && - other.replyId == this.replyId && - other.title == this.title && - other.url == this.url && - other.customThumbnail == this.customThumbnail && - other.body == this.body); -} - -class DraftsCompanion extends UpdateCompanion { - final Value id; - final Value draftType; - final Value existingId; - final Value replyId; - final Value title; - final Value url; - final Value customThumbnail; - final Value body; - const DraftsCompanion({ - this.id = const Value.absent(), - this.draftType = const Value.absent(), - this.existingId = const Value.absent(), - this.replyId = const Value.absent(), - this.title = const Value.absent(), - this.url = const Value.absent(), - this.customThumbnail = const Value.absent(), - this.body = const Value.absent(), - }); - DraftsCompanion.insert({ - this.id = const Value.absent(), - required String draftType, - this.existingId = const Value.absent(), - this.replyId = const Value.absent(), - this.title = const Value.absent(), - this.url = const Value.absent(), - this.customThumbnail = const Value.absent(), - this.body = const Value.absent(), - }) : draftType = Value(draftType); - static Insertable custom({ - Expression? id, - Expression? draftType, - Expression? existingId, - Expression? replyId, - Expression? title, - Expression? url, - Expression? customThumbnail, - Expression? body, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (draftType != null) 'draft_type': draftType, - if (existingId != null) 'existing_id': existingId, - if (replyId != null) 'reply_id': replyId, - if (title != null) 'title': title, - if (url != null) 'url': url, - if (customThumbnail != null) 'custom_thumbnail': customThumbnail, - if (body != null) 'body': body, - }); - } - - DraftsCompanion copyWith( - {Value? id, - Value? draftType, - Value? existingId, - Value? replyId, - Value? title, - Value? url, - Value? customThumbnail, - Value? body}) { - return DraftsCompanion( - id: id ?? this.id, - draftType: draftType ?? this.draftType, - existingId: existingId ?? this.existingId, - replyId: replyId ?? this.replyId, - title: title ?? this.title, - url: url ?? this.url, - customThumbnail: customThumbnail ?? this.customThumbnail, - body: body ?? this.body, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (draftType.present) { - map['draft_type'] = Variable(draftType.value); - } - if (existingId.present) { - map['existing_id'] = Variable(existingId.value); - } - if (replyId.present) { - map['reply_id'] = Variable(replyId.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (url.present) { - map['url'] = Variable(url.value); - } - if (customThumbnail.present) { - map['custom_thumbnail'] = Variable(customThumbnail.value); - } - if (body.present) { - map['body'] = Variable(body.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('DraftsCompanion(') - ..write('id: $id, ') - ..write('draftType: $draftType, ') - ..write('existingId: $existingId, ') - ..write('replyId: $replyId, ') - ..write('title: $title, ') - ..write('url: $url, ') - ..write('customThumbnail: $customThumbnail, ') - ..write('body: $body') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV4 extends GeneratedDatabase { - DatabaseAtV4(QueryExecutor e) : super(e); - late final Accounts accounts = Accounts(this); - late final Favorites favorites = Favorites(this); - late final LocalSubscriptions localSubscriptions = LocalSubscriptions(this); - late final UserLabels userLabels = UserLabels(this); - late final Drafts drafts = Drafts(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [accounts, favorites, localSubscriptions, userLabels, drafts]; - @override - int get schemaVersion => 4; -} diff --git a/test/drift/thunder/generated/schema_v5.dart b/test/drift/thunder/generated/schema_v5.dart deleted file mode 100644 index 194afa624..000000000 --- a/test/drift/thunder/generated/schema_v5.dart +++ /dev/null @@ -1,1317 +0,0 @@ -// dart format width=80 -// GENERATED BY drift_dev, DO NOT MODIFY. -// ignore_for_file: type=lint,unused_import -// -import 'package:drift/drift.dart'; - -class Accounts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Accounts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn jwt = GeneratedColumn( - 'jwt', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn instance = GeneratedColumn( - 'instance', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn anonymous = GeneratedColumn( - 'anonymous', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("anonymous" IN (0, 1))'), - defaultValue: const CustomExpression('0')); - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn listIndex = GeneratedColumn( - 'list_index', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const CustomExpression('-1')); - @override - List get $columns => - [id, username, jwt, instance, anonymous, userId, listIndex]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'accounts'; - @override - Set get $primaryKey => {id}; - @override - AccountsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AccountsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username']), - jwt: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}jwt']), - instance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}instance']), - anonymous: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}anonymous'])!, - userId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}user_id']), - listIndex: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}list_index'])!, - ); - } - - @override - Accounts createAlias(String alias) { - return Accounts(attachedDatabase, alias); - } -} - -class AccountsData extends DataClass implements Insertable { - final int id; - final String? username; - final String? jwt; - final String? instance; - final bool anonymous; - final int? userId; - final int listIndex; - const AccountsData( - {required this.id, - this.username, - this.jwt, - this.instance, - required this.anonymous, - this.userId, - required this.listIndex}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - if (!nullToAbsent || username != null) { - map['username'] = Variable(username); - } - if (!nullToAbsent || jwt != null) { - map['jwt'] = Variable(jwt); - } - if (!nullToAbsent || instance != null) { - map['instance'] = Variable(instance); - } - map['anonymous'] = Variable(anonymous); - if (!nullToAbsent || userId != null) { - map['user_id'] = Variable(userId); - } - map['list_index'] = Variable(listIndex); - return map; - } - - AccountsCompanion toCompanion(bool nullToAbsent) { - return AccountsCompanion( - id: Value(id), - username: username == null && nullToAbsent - ? const Value.absent() - : Value(username), - jwt: jwt == null && nullToAbsent ? const Value.absent() : Value(jwt), - instance: instance == null && nullToAbsent - ? const Value.absent() - : Value(instance), - anonymous: Value(anonymous), - userId: - userId == null && nullToAbsent ? const Value.absent() : Value(userId), - listIndex: Value(listIndex), - ); - } - - factory AccountsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AccountsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - jwt: serializer.fromJson(json['jwt']), - instance: serializer.fromJson(json['instance']), - anonymous: serializer.fromJson(json['anonymous']), - userId: serializer.fromJson(json['userId']), - listIndex: serializer.fromJson(json['listIndex']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'jwt': serializer.toJson(jwt), - 'instance': serializer.toJson(instance), - 'anonymous': serializer.toJson(anonymous), - 'userId': serializer.toJson(userId), - 'listIndex': serializer.toJson(listIndex), - }; - } - - AccountsData copyWith( - {int? id, - Value username = const Value.absent(), - Value jwt = const Value.absent(), - Value instance = const Value.absent(), - bool? anonymous, - Value userId = const Value.absent(), - int? listIndex}) => - AccountsData( - id: id ?? this.id, - username: username.present ? username.value : this.username, - jwt: jwt.present ? jwt.value : this.jwt, - instance: instance.present ? instance.value : this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId.present ? userId.value : this.userId, - listIndex: listIndex ?? this.listIndex, - ); - AccountsData copyWithCompanion(AccountsCompanion data) { - return AccountsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - jwt: data.jwt.present ? data.jwt.value : this.jwt, - instance: data.instance.present ? data.instance.value : this.instance, - anonymous: data.anonymous.present ? data.anonymous.value : this.anonymous, - userId: data.userId.present ? data.userId.value : this.userId, - listIndex: data.listIndex.present ? data.listIndex.value : this.listIndex, - ); - } - - @override - String toString() { - return (StringBuffer('AccountsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId, ') - ..write('listIndex: $listIndex') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(id, username, jwt, instance, anonymous, userId, listIndex); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AccountsData && - other.id == this.id && - other.username == this.username && - other.jwt == this.jwt && - other.instance == this.instance && - other.anonymous == this.anonymous && - other.userId == this.userId && - other.listIndex == this.listIndex); -} - -class AccountsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value jwt; - final Value instance; - final Value anonymous; - final Value userId; - final Value listIndex; - const AccountsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - this.listIndex = const Value.absent(), - }); - AccountsCompanion.insert({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - this.listIndex = const Value.absent(), - }); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? jwt, - Expression? instance, - Expression? anonymous, - Expression? userId, - Expression? listIndex, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (jwt != null) 'jwt': jwt, - if (instance != null) 'instance': instance, - if (anonymous != null) 'anonymous': anonymous, - if (userId != null) 'user_id': userId, - if (listIndex != null) 'list_index': listIndex, - }); - } - - AccountsCompanion copyWith( - {Value? id, - Value? username, - Value? jwt, - Value? instance, - Value? anonymous, - Value? userId, - Value? listIndex}) { - return AccountsCompanion( - id: id ?? this.id, - username: username ?? this.username, - jwt: jwt ?? this.jwt, - instance: instance ?? this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId ?? this.userId, - listIndex: listIndex ?? this.listIndex, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (jwt.present) { - map['jwt'] = Variable(jwt.value); - } - if (instance.present) { - map['instance'] = Variable(instance.value); - } - if (anonymous.present) { - map['anonymous'] = Variable(anonymous.value); - } - if (userId.present) { - map['user_id'] = Variable(userId.value); - } - if (listIndex.present) { - map['list_index'] = Variable(listIndex.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AccountsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId, ') - ..write('listIndex: $listIndex') - ..write(')')) - .toString(); - } -} - -class Favorites extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Favorites(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - late final GeneratedColumn communityId = GeneratedColumn( - 'community_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, accountId, communityId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'favorites'; - @override - Set get $primaryKey => {id}; - @override - FavoritesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return FavoritesData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - accountId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}account_id'])!, - communityId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}community_id'])!, - ); - } - - @override - Favorites createAlias(String alias) { - return Favorites(attachedDatabase, alias); - } -} - -class FavoritesData extends DataClass implements Insertable { - final int id; - final int accountId; - final int communityId; - const FavoritesData( - {required this.id, required this.accountId, required this.communityId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['account_id'] = Variable(accountId); - map['community_id'] = Variable(communityId); - return map; - } - - FavoritesCompanion toCompanion(bool nullToAbsent) { - return FavoritesCompanion( - id: Value(id), - accountId: Value(accountId), - communityId: Value(communityId), - ); - } - - factory FavoritesData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return FavoritesData( - id: serializer.fromJson(json['id']), - accountId: serializer.fromJson(json['accountId']), - communityId: serializer.fromJson(json['communityId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'accountId': serializer.toJson(accountId), - 'communityId': serializer.toJson(communityId), - }; - } - - FavoritesData copyWith({int? id, int? accountId, int? communityId}) => - FavoritesData( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - FavoritesData copyWithCompanion(FavoritesCompanion data) { - return FavoritesData( - id: data.id.present ? data.id.value : this.id, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - communityId: - data.communityId.present ? data.communityId.value : this.communityId, - ); - } - - @override - String toString() { - return (StringBuffer('FavoritesData(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, accountId, communityId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is FavoritesData && - other.id == this.id && - other.accountId == this.accountId && - other.communityId == this.communityId); -} - -class FavoritesCompanion extends UpdateCompanion { - final Value id; - final Value accountId; - final Value communityId; - const FavoritesCompanion({ - this.id = const Value.absent(), - this.accountId = const Value.absent(), - this.communityId = const Value.absent(), - }); - FavoritesCompanion.insert({ - this.id = const Value.absent(), - required int accountId, - required int communityId, - }) : accountId = Value(accountId), - communityId = Value(communityId); - static Insertable custom({ - Expression? id, - Expression? accountId, - Expression? communityId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (accountId != null) 'account_id': accountId, - if (communityId != null) 'community_id': communityId, - }); - } - - FavoritesCompanion copyWith( - {Value? id, Value? accountId, Value? communityId}) { - return FavoritesCompanion( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (communityId.present) { - map['community_id'] = Variable(communityId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('FavoritesCompanion(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } -} - -class LocalSubscriptions extends Table - with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - LocalSubscriptions(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn actorId = GeneratedColumn( - 'actor_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn icon = GeneratedColumn( - 'icon', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => [id, name, title, actorId, icon]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'local_subscriptions'; - @override - Set get $primaryKey => {id}; - @override - LocalSubscriptionsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return LocalSubscriptionsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title'])!, - actorId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}actor_id'])!, - icon: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}icon']), - ); - } - - @override - LocalSubscriptions createAlias(String alias) { - return LocalSubscriptions(attachedDatabase, alias); - } -} - -class LocalSubscriptionsData extends DataClass - implements Insertable { - final int id; - final String name; - final String title; - final String actorId; - final String? icon; - const LocalSubscriptionsData( - {required this.id, - required this.name, - required this.title, - required this.actorId, - this.icon}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['title'] = Variable(title); - map['actor_id'] = Variable(actorId); - if (!nullToAbsent || icon != null) { - map['icon'] = Variable(icon); - } - return map; - } - - LocalSubscriptionsCompanion toCompanion(bool nullToAbsent) { - return LocalSubscriptionsCompanion( - id: Value(id), - name: Value(name), - title: Value(title), - actorId: Value(actorId), - icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon), - ); - } - - factory LocalSubscriptionsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return LocalSubscriptionsData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - title: serializer.fromJson(json['title']), - actorId: serializer.fromJson(json['actorId']), - icon: serializer.fromJson(json['icon']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'title': serializer.toJson(title), - 'actorId': serializer.toJson(actorId), - 'icon': serializer.toJson(icon), - }; - } - - LocalSubscriptionsData copyWith( - {int? id, - String? name, - String? title, - String? actorId, - Value icon = const Value.absent()}) => - LocalSubscriptionsData( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon.present ? icon.value : this.icon, - ); - LocalSubscriptionsData copyWithCompanion(LocalSubscriptionsCompanion data) { - return LocalSubscriptionsData( - id: data.id.present ? data.id.value : this.id, - name: data.name.present ? data.name.value : this.name, - title: data.title.present ? data.title.value : this.title, - actorId: data.actorId.present ? data.actorId.value : this.actorId, - icon: data.icon.present ? data.icon.value : this.icon, - ); - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, title, actorId, icon); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is LocalSubscriptionsData && - other.id == this.id && - other.name == this.name && - other.title == this.title && - other.actorId == this.actorId && - other.icon == this.icon); -} - -class LocalSubscriptionsCompanion - extends UpdateCompanion { - final Value id; - final Value name; - final Value title; - final Value actorId; - final Value icon; - const LocalSubscriptionsCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.title = const Value.absent(), - this.actorId = const Value.absent(), - this.icon = const Value.absent(), - }); - LocalSubscriptionsCompanion.insert({ - this.id = const Value.absent(), - required String name, - required String title, - required String actorId, - this.icon = const Value.absent(), - }) : name = Value(name), - title = Value(title), - actorId = Value(actorId); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? title, - Expression? actorId, - Expression? icon, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (title != null) 'title': title, - if (actorId != null) 'actor_id': actorId, - if (icon != null) 'icon': icon, - }); - } - - LocalSubscriptionsCompanion copyWith( - {Value? id, - Value? name, - Value? title, - Value? actorId, - Value? icon}) { - return LocalSubscriptionsCompanion( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon ?? this.icon, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (actorId.present) { - map['actor_id'] = Variable(actorId.value); - } - if (icon.present) { - map['icon'] = Variable(icon.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } -} - -class UserLabels extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - UserLabels(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn label = GeneratedColumn( - 'label', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - @override - List get $columns => [id, username, label]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'user_labels'; - @override - Set get $primaryKey => {id}; - @override - UserLabelsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return UserLabelsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username'])!, - label: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}label'])!, - ); - } - - @override - UserLabels createAlias(String alias) { - return UserLabels(attachedDatabase, alias); - } -} - -class UserLabelsData extends DataClass implements Insertable { - final int id; - final String username; - final String label; - const UserLabelsData( - {required this.id, required this.username, required this.label}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['username'] = Variable(username); - map['label'] = Variable(label); - return map; - } - - UserLabelsCompanion toCompanion(bool nullToAbsent) { - return UserLabelsCompanion( - id: Value(id), - username: Value(username), - label: Value(label), - ); - } - - factory UserLabelsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return UserLabelsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - label: serializer.fromJson(json['label']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'label': serializer.toJson(label), - }; - } - - UserLabelsData copyWith({int? id, String? username, String? label}) => - UserLabelsData( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - UserLabelsData copyWithCompanion(UserLabelsCompanion data) { - return UserLabelsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - label: data.label.present ? data.label.value : this.label, - ); - } - - @override - String toString() { - return (StringBuffer('UserLabelsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, username, label); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is UserLabelsData && - other.id == this.id && - other.username == this.username && - other.label == this.label); -} - -class UserLabelsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value label; - const UserLabelsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.label = const Value.absent(), - }); - UserLabelsCompanion.insert({ - this.id = const Value.absent(), - required String username, - required String label, - }) : username = Value(username), - label = Value(label); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? label, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (label != null) 'label': label, - }); - } - - UserLabelsCompanion copyWith( - {Value? id, Value? username, Value? label}) { - return UserLabelsCompanion( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (label.present) { - map['label'] = Variable(label.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('UserLabelsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } -} - -class Drafts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Drafts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn draftType = GeneratedColumn( - 'draft_type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn existingId = GeneratedColumn( - 'existing_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn replyId = GeneratedColumn( - 'reply_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn url = GeneratedColumn( - 'url', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn customThumbnail = GeneratedColumn( - 'custom_thumbnail', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn body = GeneratedColumn( - 'body', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => - [id, draftType, existingId, replyId, title, url, customThumbnail, body]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'drafts'; - @override - Set get $primaryKey => {id}; - @override - DraftsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return DraftsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - draftType: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}draft_type'])!, - existingId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}existing_id']), - replyId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}reply_id']), - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title']), - url: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}url']), - customThumbnail: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}custom_thumbnail']), - body: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}body']), - ); - } - - @override - Drafts createAlias(String alias) { - return Drafts(attachedDatabase, alias); - } -} - -class DraftsData extends DataClass implements Insertable { - final int id; - final String draftType; - final int? existingId; - final int? replyId; - final String? title; - final String? url; - final String? customThumbnail; - final String? body; - const DraftsData( - {required this.id, - required this.draftType, - this.existingId, - this.replyId, - this.title, - this.url, - this.customThumbnail, - this.body}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['draft_type'] = Variable(draftType); - if (!nullToAbsent || existingId != null) { - map['existing_id'] = Variable(existingId); - } - if (!nullToAbsent || replyId != null) { - map['reply_id'] = Variable(replyId); - } - if (!nullToAbsent || title != null) { - map['title'] = Variable(title); - } - if (!nullToAbsent || url != null) { - map['url'] = Variable(url); - } - if (!nullToAbsent || customThumbnail != null) { - map['custom_thumbnail'] = Variable(customThumbnail); - } - if (!nullToAbsent || body != null) { - map['body'] = Variable(body); - } - return map; - } - - DraftsCompanion toCompanion(bool nullToAbsent) { - return DraftsCompanion( - id: Value(id), - draftType: Value(draftType), - existingId: existingId == null && nullToAbsent - ? const Value.absent() - : Value(existingId), - replyId: replyId == null && nullToAbsent - ? const Value.absent() - : Value(replyId), - title: - title == null && nullToAbsent ? const Value.absent() : Value(title), - url: url == null && nullToAbsent ? const Value.absent() : Value(url), - customThumbnail: customThumbnail == null && nullToAbsent - ? const Value.absent() - : Value(customThumbnail), - body: body == null && nullToAbsent ? const Value.absent() : Value(body), - ); - } - - factory DraftsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return DraftsData( - id: serializer.fromJson(json['id']), - draftType: serializer.fromJson(json['draftType']), - existingId: serializer.fromJson(json['existingId']), - replyId: serializer.fromJson(json['replyId']), - title: serializer.fromJson(json['title']), - url: serializer.fromJson(json['url']), - customThumbnail: serializer.fromJson(json['customThumbnail']), - body: serializer.fromJson(json['body']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'draftType': serializer.toJson(draftType), - 'existingId': serializer.toJson(existingId), - 'replyId': serializer.toJson(replyId), - 'title': serializer.toJson(title), - 'url': serializer.toJson(url), - 'customThumbnail': serializer.toJson(customThumbnail), - 'body': serializer.toJson(body), - }; - } - - DraftsData copyWith( - {int? id, - String? draftType, - Value existingId = const Value.absent(), - Value replyId = const Value.absent(), - Value title = const Value.absent(), - Value url = const Value.absent(), - Value customThumbnail = const Value.absent(), - Value body = const Value.absent()}) => - DraftsData( - id: id ?? this.id, - draftType: draftType ?? this.draftType, - existingId: existingId.present ? existingId.value : this.existingId, - replyId: replyId.present ? replyId.value : this.replyId, - title: title.present ? title.value : this.title, - url: url.present ? url.value : this.url, - customThumbnail: customThumbnail.present - ? customThumbnail.value - : this.customThumbnail, - body: body.present ? body.value : this.body, - ); - DraftsData copyWithCompanion(DraftsCompanion data) { - return DraftsData( - id: data.id.present ? data.id.value : this.id, - draftType: data.draftType.present ? data.draftType.value : this.draftType, - existingId: - data.existingId.present ? data.existingId.value : this.existingId, - replyId: data.replyId.present ? data.replyId.value : this.replyId, - title: data.title.present ? data.title.value : this.title, - url: data.url.present ? data.url.value : this.url, - customThumbnail: data.customThumbnail.present - ? data.customThumbnail.value - : this.customThumbnail, - body: data.body.present ? data.body.value : this.body, - ); - } - - @override - String toString() { - return (StringBuffer('DraftsData(') - ..write('id: $id, ') - ..write('draftType: $draftType, ') - ..write('existingId: $existingId, ') - ..write('replyId: $replyId, ') - ..write('title: $title, ') - ..write('url: $url, ') - ..write('customThumbnail: $customThumbnail, ') - ..write('body: $body') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash( - id, draftType, existingId, replyId, title, url, customThumbnail, body); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is DraftsData && - other.id == this.id && - other.draftType == this.draftType && - other.existingId == this.existingId && - other.replyId == this.replyId && - other.title == this.title && - other.url == this.url && - other.customThumbnail == this.customThumbnail && - other.body == this.body); -} - -class DraftsCompanion extends UpdateCompanion { - final Value id; - final Value draftType; - final Value existingId; - final Value replyId; - final Value title; - final Value url; - final Value customThumbnail; - final Value body; - const DraftsCompanion({ - this.id = const Value.absent(), - this.draftType = const Value.absent(), - this.existingId = const Value.absent(), - this.replyId = const Value.absent(), - this.title = const Value.absent(), - this.url = const Value.absent(), - this.customThumbnail = const Value.absent(), - this.body = const Value.absent(), - }); - DraftsCompanion.insert({ - this.id = const Value.absent(), - required String draftType, - this.existingId = const Value.absent(), - this.replyId = const Value.absent(), - this.title = const Value.absent(), - this.url = const Value.absent(), - this.customThumbnail = const Value.absent(), - this.body = const Value.absent(), - }) : draftType = Value(draftType); - static Insertable custom({ - Expression? id, - Expression? draftType, - Expression? existingId, - Expression? replyId, - Expression? title, - Expression? url, - Expression? customThumbnail, - Expression? body, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (draftType != null) 'draft_type': draftType, - if (existingId != null) 'existing_id': existingId, - if (replyId != null) 'reply_id': replyId, - if (title != null) 'title': title, - if (url != null) 'url': url, - if (customThumbnail != null) 'custom_thumbnail': customThumbnail, - if (body != null) 'body': body, - }); - } - - DraftsCompanion copyWith( - {Value? id, - Value? draftType, - Value? existingId, - Value? replyId, - Value? title, - Value? url, - Value? customThumbnail, - Value? body}) { - return DraftsCompanion( - id: id ?? this.id, - draftType: draftType ?? this.draftType, - existingId: existingId ?? this.existingId, - replyId: replyId ?? this.replyId, - title: title ?? this.title, - url: url ?? this.url, - customThumbnail: customThumbnail ?? this.customThumbnail, - body: body ?? this.body, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (draftType.present) { - map['draft_type'] = Variable(draftType.value); - } - if (existingId.present) { - map['existing_id'] = Variable(existingId.value); - } - if (replyId.present) { - map['reply_id'] = Variable(replyId.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (url.present) { - map['url'] = Variable(url.value); - } - if (customThumbnail.present) { - map['custom_thumbnail'] = Variable(customThumbnail.value); - } - if (body.present) { - map['body'] = Variable(body.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('DraftsCompanion(') - ..write('id: $id, ') - ..write('draftType: $draftType, ') - ..write('existingId: $existingId, ') - ..write('replyId: $replyId, ') - ..write('title: $title, ') - ..write('url: $url, ') - ..write('customThumbnail: $customThumbnail, ') - ..write('body: $body') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV5 extends GeneratedDatabase { - DatabaseAtV5(QueryExecutor e) : super(e); - late final Accounts accounts = Accounts(this); - late final Favorites favorites = Favorites(this); - late final LocalSubscriptions localSubscriptions = LocalSubscriptions(this); - late final UserLabels userLabels = UserLabels(this); - late final Drafts drafts = Drafts(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [accounts, favorites, localSubscriptions, userLabels, drafts]; - @override - int get schemaVersion => 5; -} diff --git a/test/drift/thunder/generated/schema_v6.dart b/test/drift/thunder/generated/schema_v6.dart deleted file mode 100644 index 66e1cad4b..000000000 --- a/test/drift/thunder/generated/schema_v6.dart +++ /dev/null @@ -1,1357 +0,0 @@ -// dart format width=80 -// GENERATED BY drift_dev, DO NOT MODIFY. -// ignore_for_file: type=lint,unused_import -// -import 'package:drift/drift.dart'; - -class Accounts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Accounts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn jwt = GeneratedColumn( - 'jwt', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn instance = GeneratedColumn( - 'instance', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn anonymous = GeneratedColumn( - 'anonymous', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("anonymous" IN (0, 1))'), - defaultValue: const CustomExpression('0')); - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn listIndex = GeneratedColumn( - 'list_index', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const CustomExpression('-1')); - @override - List get $columns => - [id, username, jwt, instance, anonymous, userId, listIndex]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'accounts'; - @override - Set get $primaryKey => {id}; - @override - AccountsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AccountsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username']), - jwt: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}jwt']), - instance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}instance']), - anonymous: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}anonymous'])!, - userId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}user_id']), - listIndex: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}list_index'])!, - ); - } - - @override - Accounts createAlias(String alias) { - return Accounts(attachedDatabase, alias); - } -} - -class AccountsData extends DataClass implements Insertable { - final int id; - final String? username; - final String? jwt; - final String? instance; - final bool anonymous; - final int? userId; - final int listIndex; - const AccountsData( - {required this.id, - this.username, - this.jwt, - this.instance, - required this.anonymous, - this.userId, - required this.listIndex}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - if (!nullToAbsent || username != null) { - map['username'] = Variable(username); - } - if (!nullToAbsent || jwt != null) { - map['jwt'] = Variable(jwt); - } - if (!nullToAbsent || instance != null) { - map['instance'] = Variable(instance); - } - map['anonymous'] = Variable(anonymous); - if (!nullToAbsent || userId != null) { - map['user_id'] = Variable(userId); - } - map['list_index'] = Variable(listIndex); - return map; - } - - AccountsCompanion toCompanion(bool nullToAbsent) { - return AccountsCompanion( - id: Value(id), - username: username == null && nullToAbsent - ? const Value.absent() - : Value(username), - jwt: jwt == null && nullToAbsent ? const Value.absent() : Value(jwt), - instance: instance == null && nullToAbsent - ? const Value.absent() - : Value(instance), - anonymous: Value(anonymous), - userId: - userId == null && nullToAbsent ? const Value.absent() : Value(userId), - listIndex: Value(listIndex), - ); - } - - factory AccountsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AccountsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - jwt: serializer.fromJson(json['jwt']), - instance: serializer.fromJson(json['instance']), - anonymous: serializer.fromJson(json['anonymous']), - userId: serializer.fromJson(json['userId']), - listIndex: serializer.fromJson(json['listIndex']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'jwt': serializer.toJson(jwt), - 'instance': serializer.toJson(instance), - 'anonymous': serializer.toJson(anonymous), - 'userId': serializer.toJson(userId), - 'listIndex': serializer.toJson(listIndex), - }; - } - - AccountsData copyWith( - {int? id, - Value username = const Value.absent(), - Value jwt = const Value.absent(), - Value instance = const Value.absent(), - bool? anonymous, - Value userId = const Value.absent(), - int? listIndex}) => - AccountsData( - id: id ?? this.id, - username: username.present ? username.value : this.username, - jwt: jwt.present ? jwt.value : this.jwt, - instance: instance.present ? instance.value : this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId.present ? userId.value : this.userId, - listIndex: listIndex ?? this.listIndex, - ); - AccountsData copyWithCompanion(AccountsCompanion data) { - return AccountsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - jwt: data.jwt.present ? data.jwt.value : this.jwt, - instance: data.instance.present ? data.instance.value : this.instance, - anonymous: data.anonymous.present ? data.anonymous.value : this.anonymous, - userId: data.userId.present ? data.userId.value : this.userId, - listIndex: data.listIndex.present ? data.listIndex.value : this.listIndex, - ); - } - - @override - String toString() { - return (StringBuffer('AccountsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId, ') - ..write('listIndex: $listIndex') - ..write(')')) - .toString(); - } - - @override - int get hashCode => - Object.hash(id, username, jwt, instance, anonymous, userId, listIndex); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AccountsData && - other.id == this.id && - other.username == this.username && - other.jwt == this.jwt && - other.instance == this.instance && - other.anonymous == this.anonymous && - other.userId == this.userId && - other.listIndex == this.listIndex); -} - -class AccountsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value jwt; - final Value instance; - final Value anonymous; - final Value userId; - final Value listIndex; - const AccountsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - this.listIndex = const Value.absent(), - }); - AccountsCompanion.insert({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - this.listIndex = const Value.absent(), - }); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? jwt, - Expression? instance, - Expression? anonymous, - Expression? userId, - Expression? listIndex, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (jwt != null) 'jwt': jwt, - if (instance != null) 'instance': instance, - if (anonymous != null) 'anonymous': anonymous, - if (userId != null) 'user_id': userId, - if (listIndex != null) 'list_index': listIndex, - }); - } - - AccountsCompanion copyWith( - {Value? id, - Value? username, - Value? jwt, - Value? instance, - Value? anonymous, - Value? userId, - Value? listIndex}) { - return AccountsCompanion( - id: id ?? this.id, - username: username ?? this.username, - jwt: jwt ?? this.jwt, - instance: instance ?? this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId ?? this.userId, - listIndex: listIndex ?? this.listIndex, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (jwt.present) { - map['jwt'] = Variable(jwt.value); - } - if (instance.present) { - map['instance'] = Variable(instance.value); - } - if (anonymous.present) { - map['anonymous'] = Variable(anonymous.value); - } - if (userId.present) { - map['user_id'] = Variable(userId.value); - } - if (listIndex.present) { - map['list_index'] = Variable(listIndex.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AccountsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId, ') - ..write('listIndex: $listIndex') - ..write(')')) - .toString(); - } -} - -class Favorites extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Favorites(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - late final GeneratedColumn communityId = GeneratedColumn( - 'community_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, accountId, communityId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'favorites'; - @override - Set get $primaryKey => {id}; - @override - FavoritesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return FavoritesData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - accountId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}account_id'])!, - communityId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}community_id'])!, - ); - } - - @override - Favorites createAlias(String alias) { - return Favorites(attachedDatabase, alias); - } -} - -class FavoritesData extends DataClass implements Insertable { - final int id; - final int accountId; - final int communityId; - const FavoritesData( - {required this.id, required this.accountId, required this.communityId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['account_id'] = Variable(accountId); - map['community_id'] = Variable(communityId); - return map; - } - - FavoritesCompanion toCompanion(bool nullToAbsent) { - return FavoritesCompanion( - id: Value(id), - accountId: Value(accountId), - communityId: Value(communityId), - ); - } - - factory FavoritesData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return FavoritesData( - id: serializer.fromJson(json['id']), - accountId: serializer.fromJson(json['accountId']), - communityId: serializer.fromJson(json['communityId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'accountId': serializer.toJson(accountId), - 'communityId': serializer.toJson(communityId), - }; - } - - FavoritesData copyWith({int? id, int? accountId, int? communityId}) => - FavoritesData( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - FavoritesData copyWithCompanion(FavoritesCompanion data) { - return FavoritesData( - id: data.id.present ? data.id.value : this.id, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - communityId: - data.communityId.present ? data.communityId.value : this.communityId, - ); - } - - @override - String toString() { - return (StringBuffer('FavoritesData(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, accountId, communityId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is FavoritesData && - other.id == this.id && - other.accountId == this.accountId && - other.communityId == this.communityId); -} - -class FavoritesCompanion extends UpdateCompanion { - final Value id; - final Value accountId; - final Value communityId; - const FavoritesCompanion({ - this.id = const Value.absent(), - this.accountId = const Value.absent(), - this.communityId = const Value.absent(), - }); - FavoritesCompanion.insert({ - this.id = const Value.absent(), - required int accountId, - required int communityId, - }) : accountId = Value(accountId), - communityId = Value(communityId); - static Insertable custom({ - Expression? id, - Expression? accountId, - Expression? communityId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (accountId != null) 'account_id': accountId, - if (communityId != null) 'community_id': communityId, - }); - } - - FavoritesCompanion copyWith( - {Value? id, Value? accountId, Value? communityId}) { - return FavoritesCompanion( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (communityId.present) { - map['community_id'] = Variable(communityId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('FavoritesCompanion(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } -} - -class LocalSubscriptions extends Table - with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - LocalSubscriptions(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn actorId = GeneratedColumn( - 'actor_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn icon = GeneratedColumn( - 'icon', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => [id, name, title, actorId, icon]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'local_subscriptions'; - @override - Set get $primaryKey => {id}; - @override - LocalSubscriptionsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return LocalSubscriptionsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title'])!, - actorId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}actor_id'])!, - icon: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}icon']), - ); - } - - @override - LocalSubscriptions createAlias(String alias) { - return LocalSubscriptions(attachedDatabase, alias); - } -} - -class LocalSubscriptionsData extends DataClass - implements Insertable { - final int id; - final String name; - final String title; - final String actorId; - final String? icon; - const LocalSubscriptionsData( - {required this.id, - required this.name, - required this.title, - required this.actorId, - this.icon}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['title'] = Variable(title); - map['actor_id'] = Variable(actorId); - if (!nullToAbsent || icon != null) { - map['icon'] = Variable(icon); - } - return map; - } - - LocalSubscriptionsCompanion toCompanion(bool nullToAbsent) { - return LocalSubscriptionsCompanion( - id: Value(id), - name: Value(name), - title: Value(title), - actorId: Value(actorId), - icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon), - ); - } - - factory LocalSubscriptionsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return LocalSubscriptionsData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - title: serializer.fromJson(json['title']), - actorId: serializer.fromJson(json['actorId']), - icon: serializer.fromJson(json['icon']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'title': serializer.toJson(title), - 'actorId': serializer.toJson(actorId), - 'icon': serializer.toJson(icon), - }; - } - - LocalSubscriptionsData copyWith( - {int? id, - String? name, - String? title, - String? actorId, - Value icon = const Value.absent()}) => - LocalSubscriptionsData( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon.present ? icon.value : this.icon, - ); - LocalSubscriptionsData copyWithCompanion(LocalSubscriptionsCompanion data) { - return LocalSubscriptionsData( - id: data.id.present ? data.id.value : this.id, - name: data.name.present ? data.name.value : this.name, - title: data.title.present ? data.title.value : this.title, - actorId: data.actorId.present ? data.actorId.value : this.actorId, - icon: data.icon.present ? data.icon.value : this.icon, - ); - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, title, actorId, icon); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is LocalSubscriptionsData && - other.id == this.id && - other.name == this.name && - other.title == this.title && - other.actorId == this.actorId && - other.icon == this.icon); -} - -class LocalSubscriptionsCompanion - extends UpdateCompanion { - final Value id; - final Value name; - final Value title; - final Value actorId; - final Value icon; - const LocalSubscriptionsCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.title = const Value.absent(), - this.actorId = const Value.absent(), - this.icon = const Value.absent(), - }); - LocalSubscriptionsCompanion.insert({ - this.id = const Value.absent(), - required String name, - required String title, - required String actorId, - this.icon = const Value.absent(), - }) : name = Value(name), - title = Value(title), - actorId = Value(actorId); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? title, - Expression? actorId, - Expression? icon, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (title != null) 'title': title, - if (actorId != null) 'actor_id': actorId, - if (icon != null) 'icon': icon, - }); - } - - LocalSubscriptionsCompanion copyWith( - {Value? id, - Value? name, - Value? title, - Value? actorId, - Value? icon}) { - return LocalSubscriptionsCompanion( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon ?? this.icon, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (actorId.present) { - map['actor_id'] = Variable(actorId.value); - } - if (icon.present) { - map['icon'] = Variable(icon.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } -} - -class UserLabels extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - UserLabels(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn label = GeneratedColumn( - 'label', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - @override - List get $columns => [id, username, label]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'user_labels'; - @override - Set get $primaryKey => {id}; - @override - UserLabelsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return UserLabelsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username'])!, - label: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}label'])!, - ); - } - - @override - UserLabels createAlias(String alias) { - return UserLabels(attachedDatabase, alias); - } -} - -class UserLabelsData extends DataClass implements Insertable { - final int id; - final String username; - final String label; - const UserLabelsData( - {required this.id, required this.username, required this.label}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['username'] = Variable(username); - map['label'] = Variable(label); - return map; - } - - UserLabelsCompanion toCompanion(bool nullToAbsent) { - return UserLabelsCompanion( - id: Value(id), - username: Value(username), - label: Value(label), - ); - } - - factory UserLabelsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return UserLabelsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - label: serializer.fromJson(json['label']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'label': serializer.toJson(label), - }; - } - - UserLabelsData copyWith({int? id, String? username, String? label}) => - UserLabelsData( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - UserLabelsData copyWithCompanion(UserLabelsCompanion data) { - return UserLabelsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - label: data.label.present ? data.label.value : this.label, - ); - } - - @override - String toString() { - return (StringBuffer('UserLabelsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, username, label); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is UserLabelsData && - other.id == this.id && - other.username == this.username && - other.label == this.label); -} - -class UserLabelsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value label; - const UserLabelsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.label = const Value.absent(), - }); - UserLabelsCompanion.insert({ - this.id = const Value.absent(), - required String username, - required String label, - }) : username = Value(username), - label = Value(label); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? label, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (label != null) 'label': label, - }); - } - - UserLabelsCompanion copyWith( - {Value? id, Value? username, Value? label}) { - return UserLabelsCompanion( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (label.present) { - map['label'] = Variable(label.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('UserLabelsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } -} - -class Drafts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Drafts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn draftType = GeneratedColumn( - 'draft_type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn existingId = GeneratedColumn( - 'existing_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn replyId = GeneratedColumn( - 'reply_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn url = GeneratedColumn( - 'url', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn customThumbnail = GeneratedColumn( - 'custom_thumbnail', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn altText = GeneratedColumn( - 'alt_text', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn body = GeneratedColumn( - 'body', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => [ - id, - draftType, - existingId, - replyId, - title, - url, - customThumbnail, - altText, - body - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'drafts'; - @override - Set get $primaryKey => {id}; - @override - DraftsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return DraftsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - draftType: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}draft_type'])!, - existingId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}existing_id']), - replyId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}reply_id']), - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title']), - url: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}url']), - customThumbnail: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}custom_thumbnail']), - altText: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}alt_text']), - body: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}body']), - ); - } - - @override - Drafts createAlias(String alias) { - return Drafts(attachedDatabase, alias); - } -} - -class DraftsData extends DataClass implements Insertable { - final int id; - final String draftType; - final int? existingId; - final int? replyId; - final String? title; - final String? url; - final String? customThumbnail; - final String? altText; - final String? body; - const DraftsData( - {required this.id, - required this.draftType, - this.existingId, - this.replyId, - this.title, - this.url, - this.customThumbnail, - this.altText, - this.body}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['draft_type'] = Variable(draftType); - if (!nullToAbsent || existingId != null) { - map['existing_id'] = Variable(existingId); - } - if (!nullToAbsent || replyId != null) { - map['reply_id'] = Variable(replyId); - } - if (!nullToAbsent || title != null) { - map['title'] = Variable(title); - } - if (!nullToAbsent || url != null) { - map['url'] = Variable(url); - } - if (!nullToAbsent || customThumbnail != null) { - map['custom_thumbnail'] = Variable(customThumbnail); - } - if (!nullToAbsent || altText != null) { - map['alt_text'] = Variable(altText); - } - if (!nullToAbsent || body != null) { - map['body'] = Variable(body); - } - return map; - } - - DraftsCompanion toCompanion(bool nullToAbsent) { - return DraftsCompanion( - id: Value(id), - draftType: Value(draftType), - existingId: existingId == null && nullToAbsent - ? const Value.absent() - : Value(existingId), - replyId: replyId == null && nullToAbsent - ? const Value.absent() - : Value(replyId), - title: - title == null && nullToAbsent ? const Value.absent() : Value(title), - url: url == null && nullToAbsent ? const Value.absent() : Value(url), - customThumbnail: customThumbnail == null && nullToAbsent - ? const Value.absent() - : Value(customThumbnail), - altText: altText == null && nullToAbsent - ? const Value.absent() - : Value(altText), - body: body == null && nullToAbsent ? const Value.absent() : Value(body), - ); - } - - factory DraftsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return DraftsData( - id: serializer.fromJson(json['id']), - draftType: serializer.fromJson(json['draftType']), - existingId: serializer.fromJson(json['existingId']), - replyId: serializer.fromJson(json['replyId']), - title: serializer.fromJson(json['title']), - url: serializer.fromJson(json['url']), - customThumbnail: serializer.fromJson(json['customThumbnail']), - altText: serializer.fromJson(json['altText']), - body: serializer.fromJson(json['body']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'draftType': serializer.toJson(draftType), - 'existingId': serializer.toJson(existingId), - 'replyId': serializer.toJson(replyId), - 'title': serializer.toJson(title), - 'url': serializer.toJson(url), - 'customThumbnail': serializer.toJson(customThumbnail), - 'altText': serializer.toJson(altText), - 'body': serializer.toJson(body), - }; - } - - DraftsData copyWith( - {int? id, - String? draftType, - Value existingId = const Value.absent(), - Value replyId = const Value.absent(), - Value title = const Value.absent(), - Value url = const Value.absent(), - Value customThumbnail = const Value.absent(), - Value altText = const Value.absent(), - Value body = const Value.absent()}) => - DraftsData( - id: id ?? this.id, - draftType: draftType ?? this.draftType, - existingId: existingId.present ? existingId.value : this.existingId, - replyId: replyId.present ? replyId.value : this.replyId, - title: title.present ? title.value : this.title, - url: url.present ? url.value : this.url, - customThumbnail: customThumbnail.present - ? customThumbnail.value - : this.customThumbnail, - altText: altText.present ? altText.value : this.altText, - body: body.present ? body.value : this.body, - ); - DraftsData copyWithCompanion(DraftsCompanion data) { - return DraftsData( - id: data.id.present ? data.id.value : this.id, - draftType: data.draftType.present ? data.draftType.value : this.draftType, - existingId: - data.existingId.present ? data.existingId.value : this.existingId, - replyId: data.replyId.present ? data.replyId.value : this.replyId, - title: data.title.present ? data.title.value : this.title, - url: data.url.present ? data.url.value : this.url, - customThumbnail: data.customThumbnail.present - ? data.customThumbnail.value - : this.customThumbnail, - altText: data.altText.present ? data.altText.value : this.altText, - body: data.body.present ? data.body.value : this.body, - ); - } - - @override - String toString() { - return (StringBuffer('DraftsData(') - ..write('id: $id, ') - ..write('draftType: $draftType, ') - ..write('existingId: $existingId, ') - ..write('replyId: $replyId, ') - ..write('title: $title, ') - ..write('url: $url, ') - ..write('customThumbnail: $customThumbnail, ') - ..write('altText: $altText, ') - ..write('body: $body') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, draftType, existingId, replyId, title, - url, customThumbnail, altText, body); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is DraftsData && - other.id == this.id && - other.draftType == this.draftType && - other.existingId == this.existingId && - other.replyId == this.replyId && - other.title == this.title && - other.url == this.url && - other.customThumbnail == this.customThumbnail && - other.altText == this.altText && - other.body == this.body); -} - -class DraftsCompanion extends UpdateCompanion { - final Value id; - final Value draftType; - final Value existingId; - final Value replyId; - final Value title; - final Value url; - final Value customThumbnail; - final Value altText; - final Value body; - const DraftsCompanion({ - this.id = const Value.absent(), - this.draftType = const Value.absent(), - this.existingId = const Value.absent(), - this.replyId = const Value.absent(), - this.title = const Value.absent(), - this.url = const Value.absent(), - this.customThumbnail = const Value.absent(), - this.altText = const Value.absent(), - this.body = const Value.absent(), - }); - DraftsCompanion.insert({ - this.id = const Value.absent(), - required String draftType, - this.existingId = const Value.absent(), - this.replyId = const Value.absent(), - this.title = const Value.absent(), - this.url = const Value.absent(), - this.customThumbnail = const Value.absent(), - this.altText = const Value.absent(), - this.body = const Value.absent(), - }) : draftType = Value(draftType); - static Insertable custom({ - Expression? id, - Expression? draftType, - Expression? existingId, - Expression? replyId, - Expression? title, - Expression? url, - Expression? customThumbnail, - Expression? altText, - Expression? body, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (draftType != null) 'draft_type': draftType, - if (existingId != null) 'existing_id': existingId, - if (replyId != null) 'reply_id': replyId, - if (title != null) 'title': title, - if (url != null) 'url': url, - if (customThumbnail != null) 'custom_thumbnail': customThumbnail, - if (altText != null) 'alt_text': altText, - if (body != null) 'body': body, - }); - } - - DraftsCompanion copyWith( - {Value? id, - Value? draftType, - Value? existingId, - Value? replyId, - Value? title, - Value? url, - Value? customThumbnail, - Value? altText, - Value? body}) { - return DraftsCompanion( - id: id ?? this.id, - draftType: draftType ?? this.draftType, - existingId: existingId ?? this.existingId, - replyId: replyId ?? this.replyId, - title: title ?? this.title, - url: url ?? this.url, - customThumbnail: customThumbnail ?? this.customThumbnail, - altText: altText ?? this.altText, - body: body ?? this.body, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (draftType.present) { - map['draft_type'] = Variable(draftType.value); - } - if (existingId.present) { - map['existing_id'] = Variable(existingId.value); - } - if (replyId.present) { - map['reply_id'] = Variable(replyId.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (url.present) { - map['url'] = Variable(url.value); - } - if (customThumbnail.present) { - map['custom_thumbnail'] = Variable(customThumbnail.value); - } - if (altText.present) { - map['alt_text'] = Variable(altText.value); - } - if (body.present) { - map['body'] = Variable(body.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('DraftsCompanion(') - ..write('id: $id, ') - ..write('draftType: $draftType, ') - ..write('existingId: $existingId, ') - ..write('replyId: $replyId, ') - ..write('title: $title, ') - ..write('url: $url, ') - ..write('customThumbnail: $customThumbnail, ') - ..write('altText: $altText, ') - ..write('body: $body') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV6 extends GeneratedDatabase { - DatabaseAtV6(QueryExecutor e) : super(e); - late final Accounts accounts = Accounts(this); - late final Favorites favorites = Favorites(this); - late final LocalSubscriptions localSubscriptions = LocalSubscriptions(this); - late final UserLabels userLabels = UserLabels(this); - late final Drafts drafts = Drafts(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [accounts, favorites, localSubscriptions, userLabels, drafts]; - @override - int get schemaVersion => 6; -} diff --git a/test/drift/thunder/generated/schema_v7.dart b/test/drift/thunder/generated/schema_v7.dart deleted file mode 100644 index e1cafd91e..000000000 --- a/test/drift/thunder/generated/schema_v7.dart +++ /dev/null @@ -1,1388 +0,0 @@ -// dart format width=80 -// GENERATED BY drift_dev, DO NOT MODIFY. -// ignore_for_file: type=lint,unused_import -// -import 'package:drift/drift.dart'; - -class Accounts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Accounts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn jwt = GeneratedColumn( - 'jwt', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn instance = GeneratedColumn( - 'instance', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn anonymous = GeneratedColumn( - 'anonymous', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("anonymous" IN (0, 1))'), - defaultValue: const CustomExpression('0')); - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn listIndex = GeneratedColumn( - 'list_index', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const CustomExpression('-1')); - late final GeneratedColumn platform = GeneratedColumn( - 'platform', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => - [id, username, jwt, instance, anonymous, userId, listIndex, platform]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'accounts'; - @override - Set get $primaryKey => {id}; - @override - AccountsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AccountsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username']), - jwt: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}jwt']), - instance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}instance']), - anonymous: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}anonymous'])!, - userId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}user_id']), - listIndex: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}list_index'])!, - platform: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}platform']), - ); - } - - @override - Accounts createAlias(String alias) { - return Accounts(attachedDatabase, alias); - } -} - -class AccountsData extends DataClass implements Insertable { - final int id; - final String? username; - final String? jwt; - final String? instance; - final bool anonymous; - final int? userId; - final int listIndex; - final String? platform; - const AccountsData( - {required this.id, - this.username, - this.jwt, - this.instance, - required this.anonymous, - this.userId, - required this.listIndex, - this.platform}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - if (!nullToAbsent || username != null) { - map['username'] = Variable(username); - } - if (!nullToAbsent || jwt != null) { - map['jwt'] = Variable(jwt); - } - if (!nullToAbsent || instance != null) { - map['instance'] = Variable(instance); - } - map['anonymous'] = Variable(anonymous); - if (!nullToAbsent || userId != null) { - map['user_id'] = Variable(userId); - } - map['list_index'] = Variable(listIndex); - if (!nullToAbsent || platform != null) { - map['platform'] = Variable(platform); - } - return map; - } - - AccountsCompanion toCompanion(bool nullToAbsent) { - return AccountsCompanion( - id: Value(id), - username: username == null && nullToAbsent - ? const Value.absent() - : Value(username), - jwt: jwt == null && nullToAbsent ? const Value.absent() : Value(jwt), - instance: instance == null && nullToAbsent - ? const Value.absent() - : Value(instance), - anonymous: Value(anonymous), - userId: - userId == null && nullToAbsent ? const Value.absent() : Value(userId), - listIndex: Value(listIndex), - platform: platform == null && nullToAbsent - ? const Value.absent() - : Value(platform), - ); - } - - factory AccountsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AccountsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - jwt: serializer.fromJson(json['jwt']), - instance: serializer.fromJson(json['instance']), - anonymous: serializer.fromJson(json['anonymous']), - userId: serializer.fromJson(json['userId']), - listIndex: serializer.fromJson(json['listIndex']), - platform: serializer.fromJson(json['platform']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'jwt': serializer.toJson(jwt), - 'instance': serializer.toJson(instance), - 'anonymous': serializer.toJson(anonymous), - 'userId': serializer.toJson(userId), - 'listIndex': serializer.toJson(listIndex), - 'platform': serializer.toJson(platform), - }; - } - - AccountsData copyWith( - {int? id, - Value username = const Value.absent(), - Value jwt = const Value.absent(), - Value instance = const Value.absent(), - bool? anonymous, - Value userId = const Value.absent(), - int? listIndex, - Value platform = const Value.absent()}) => - AccountsData( - id: id ?? this.id, - username: username.present ? username.value : this.username, - jwt: jwt.present ? jwt.value : this.jwt, - instance: instance.present ? instance.value : this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId.present ? userId.value : this.userId, - listIndex: listIndex ?? this.listIndex, - platform: platform.present ? platform.value : this.platform, - ); - AccountsData copyWithCompanion(AccountsCompanion data) { - return AccountsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - jwt: data.jwt.present ? data.jwt.value : this.jwt, - instance: data.instance.present ? data.instance.value : this.instance, - anonymous: data.anonymous.present ? data.anonymous.value : this.anonymous, - userId: data.userId.present ? data.userId.value : this.userId, - listIndex: data.listIndex.present ? data.listIndex.value : this.listIndex, - platform: data.platform.present ? data.platform.value : this.platform, - ); - } - - @override - String toString() { - return (StringBuffer('AccountsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId, ') - ..write('listIndex: $listIndex, ') - ..write('platform: $platform') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash( - id, username, jwt, instance, anonymous, userId, listIndex, platform); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AccountsData && - other.id == this.id && - other.username == this.username && - other.jwt == this.jwt && - other.instance == this.instance && - other.anonymous == this.anonymous && - other.userId == this.userId && - other.listIndex == this.listIndex && - other.platform == this.platform); -} - -class AccountsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value jwt; - final Value instance; - final Value anonymous; - final Value userId; - final Value listIndex; - final Value platform; - const AccountsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - this.listIndex = const Value.absent(), - this.platform = const Value.absent(), - }); - AccountsCompanion.insert({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - this.listIndex = const Value.absent(), - this.platform = const Value.absent(), - }); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? jwt, - Expression? instance, - Expression? anonymous, - Expression? userId, - Expression? listIndex, - Expression? platform, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (jwt != null) 'jwt': jwt, - if (instance != null) 'instance': instance, - if (anonymous != null) 'anonymous': anonymous, - if (userId != null) 'user_id': userId, - if (listIndex != null) 'list_index': listIndex, - if (platform != null) 'platform': platform, - }); - } - - AccountsCompanion copyWith( - {Value? id, - Value? username, - Value? jwt, - Value? instance, - Value? anonymous, - Value? userId, - Value? listIndex, - Value? platform}) { - return AccountsCompanion( - id: id ?? this.id, - username: username ?? this.username, - jwt: jwt ?? this.jwt, - instance: instance ?? this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId ?? this.userId, - listIndex: listIndex ?? this.listIndex, - platform: platform ?? this.platform, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (jwt.present) { - map['jwt'] = Variable(jwt.value); - } - if (instance.present) { - map['instance'] = Variable(instance.value); - } - if (anonymous.present) { - map['anonymous'] = Variable(anonymous.value); - } - if (userId.present) { - map['user_id'] = Variable(userId.value); - } - if (listIndex.present) { - map['list_index'] = Variable(listIndex.value); - } - if (platform.present) { - map['platform'] = Variable(platform.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AccountsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId, ') - ..write('listIndex: $listIndex, ') - ..write('platform: $platform') - ..write(')')) - .toString(); - } -} - -class Favorites extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Favorites(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - late final GeneratedColumn communityId = GeneratedColumn( - 'community_id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: true); - @override - List get $columns => [id, accountId, communityId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'favorites'; - @override - Set get $primaryKey => {id}; - @override - FavoritesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return FavoritesData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - accountId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}account_id'])!, - communityId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}community_id'])!, - ); - } - - @override - Favorites createAlias(String alias) { - return Favorites(attachedDatabase, alias); - } -} - -class FavoritesData extends DataClass implements Insertable { - final int id; - final int accountId; - final int communityId; - const FavoritesData( - {required this.id, required this.accountId, required this.communityId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['account_id'] = Variable(accountId); - map['community_id'] = Variable(communityId); - return map; - } - - FavoritesCompanion toCompanion(bool nullToAbsent) { - return FavoritesCompanion( - id: Value(id), - accountId: Value(accountId), - communityId: Value(communityId), - ); - } - - factory FavoritesData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return FavoritesData( - id: serializer.fromJson(json['id']), - accountId: serializer.fromJson(json['accountId']), - communityId: serializer.fromJson(json['communityId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'accountId': serializer.toJson(accountId), - 'communityId': serializer.toJson(communityId), - }; - } - - FavoritesData copyWith({int? id, int? accountId, int? communityId}) => - FavoritesData( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - FavoritesData copyWithCompanion(FavoritesCompanion data) { - return FavoritesData( - id: data.id.present ? data.id.value : this.id, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - communityId: - data.communityId.present ? data.communityId.value : this.communityId, - ); - } - - @override - String toString() { - return (StringBuffer('FavoritesData(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, accountId, communityId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is FavoritesData && - other.id == this.id && - other.accountId == this.accountId && - other.communityId == this.communityId); -} - -class FavoritesCompanion extends UpdateCompanion { - final Value id; - final Value accountId; - final Value communityId; - const FavoritesCompanion({ - this.id = const Value.absent(), - this.accountId = const Value.absent(), - this.communityId = const Value.absent(), - }); - FavoritesCompanion.insert({ - this.id = const Value.absent(), - required int accountId, - required int communityId, - }) : accountId = Value(accountId), - communityId = Value(communityId); - static Insertable custom({ - Expression? id, - Expression? accountId, - Expression? communityId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (accountId != null) 'account_id': accountId, - if (communityId != null) 'community_id': communityId, - }); - } - - FavoritesCompanion copyWith( - {Value? id, Value? accountId, Value? communityId}) { - return FavoritesCompanion( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (communityId.present) { - map['community_id'] = Variable(communityId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('FavoritesCompanion(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } -} - -class LocalSubscriptions extends Table - with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - LocalSubscriptions(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn actorId = GeneratedColumn( - 'actor_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn icon = GeneratedColumn( - 'icon', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => [id, name, title, actorId, icon]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'local_subscriptions'; - @override - Set get $primaryKey => {id}; - @override - LocalSubscriptionsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return LocalSubscriptionsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title'])!, - actorId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}actor_id'])!, - icon: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}icon']), - ); - } - - @override - LocalSubscriptions createAlias(String alias) { - return LocalSubscriptions(attachedDatabase, alias); - } -} - -class LocalSubscriptionsData extends DataClass - implements Insertable { - final int id; - final String name; - final String title; - final String actorId; - final String? icon; - const LocalSubscriptionsData( - {required this.id, - required this.name, - required this.title, - required this.actorId, - this.icon}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['title'] = Variable(title); - map['actor_id'] = Variable(actorId); - if (!nullToAbsent || icon != null) { - map['icon'] = Variable(icon); - } - return map; - } - - LocalSubscriptionsCompanion toCompanion(bool nullToAbsent) { - return LocalSubscriptionsCompanion( - id: Value(id), - name: Value(name), - title: Value(title), - actorId: Value(actorId), - icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon), - ); - } - - factory LocalSubscriptionsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return LocalSubscriptionsData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - title: serializer.fromJson(json['title']), - actorId: serializer.fromJson(json['actorId']), - icon: serializer.fromJson(json['icon']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'title': serializer.toJson(title), - 'actorId': serializer.toJson(actorId), - 'icon': serializer.toJson(icon), - }; - } - - LocalSubscriptionsData copyWith( - {int? id, - String? name, - String? title, - String? actorId, - Value icon = const Value.absent()}) => - LocalSubscriptionsData( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon.present ? icon.value : this.icon, - ); - LocalSubscriptionsData copyWithCompanion(LocalSubscriptionsCompanion data) { - return LocalSubscriptionsData( - id: data.id.present ? data.id.value : this.id, - name: data.name.present ? data.name.value : this.name, - title: data.title.present ? data.title.value : this.title, - actorId: data.actorId.present ? data.actorId.value : this.actorId, - icon: data.icon.present ? data.icon.value : this.icon, - ); - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, title, actorId, icon); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is LocalSubscriptionsData && - other.id == this.id && - other.name == this.name && - other.title == this.title && - other.actorId == this.actorId && - other.icon == this.icon); -} - -class LocalSubscriptionsCompanion - extends UpdateCompanion { - final Value id; - final Value name; - final Value title; - final Value actorId; - final Value icon; - const LocalSubscriptionsCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.title = const Value.absent(), - this.actorId = const Value.absent(), - this.icon = const Value.absent(), - }); - LocalSubscriptionsCompanion.insert({ - this.id = const Value.absent(), - required String name, - required String title, - required String actorId, - this.icon = const Value.absent(), - }) : name = Value(name), - title = Value(title), - actorId = Value(actorId); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? title, - Expression? actorId, - Expression? icon, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (title != null) 'title': title, - if (actorId != null) 'actor_id': actorId, - if (icon != null) 'icon': icon, - }); - } - - LocalSubscriptionsCompanion copyWith( - {Value? id, - Value? name, - Value? title, - Value? actorId, - Value? icon}) { - return LocalSubscriptionsCompanion( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon ?? this.icon, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (actorId.present) { - map['actor_id'] = Variable(actorId.value); - } - if (icon.present) { - map['icon'] = Variable(icon.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } -} - -class UserLabels extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - UserLabels(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn label = GeneratedColumn( - 'label', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - @override - List get $columns => [id, username, label]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'user_labels'; - @override - Set get $primaryKey => {id}; - @override - UserLabelsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return UserLabelsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username'])!, - label: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}label'])!, - ); - } - - @override - UserLabels createAlias(String alias) { - return UserLabels(attachedDatabase, alias); - } -} - -class UserLabelsData extends DataClass implements Insertable { - final int id; - final String username; - final String label; - const UserLabelsData( - {required this.id, required this.username, required this.label}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['username'] = Variable(username); - map['label'] = Variable(label); - return map; - } - - UserLabelsCompanion toCompanion(bool nullToAbsent) { - return UserLabelsCompanion( - id: Value(id), - username: Value(username), - label: Value(label), - ); - } - - factory UserLabelsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return UserLabelsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - label: serializer.fromJson(json['label']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'label': serializer.toJson(label), - }; - } - - UserLabelsData copyWith({int? id, String? username, String? label}) => - UserLabelsData( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - UserLabelsData copyWithCompanion(UserLabelsCompanion data) { - return UserLabelsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - label: data.label.present ? data.label.value : this.label, - ); - } - - @override - String toString() { - return (StringBuffer('UserLabelsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, username, label); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is UserLabelsData && - other.id == this.id && - other.username == this.username && - other.label == this.label); -} - -class UserLabelsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value label; - const UserLabelsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.label = const Value.absent(), - }); - UserLabelsCompanion.insert({ - this.id = const Value.absent(), - required String username, - required String label, - }) : username = Value(username), - label = Value(label); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? label, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (label != null) 'label': label, - }); - } - - UserLabelsCompanion copyWith( - {Value? id, Value? username, Value? label}) { - return UserLabelsCompanion( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (label.present) { - map['label'] = Variable(label.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('UserLabelsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } -} - -class Drafts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Drafts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); - late final GeneratedColumn draftType = GeneratedColumn( - 'draft_type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - late final GeneratedColumn existingId = GeneratedColumn( - 'existing_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn replyId = GeneratedColumn( - 'reply_id', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn url = GeneratedColumn( - 'url', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn customThumbnail = GeneratedColumn( - 'custom_thumbnail', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn altText = GeneratedColumn( - 'alt_text', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - late final GeneratedColumn body = GeneratedColumn( - 'body', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - List get $columns => [ - id, - draftType, - existingId, - replyId, - title, - url, - customThumbnail, - altText, - body - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'drafts'; - @override - Set get $primaryKey => {id}; - @override - DraftsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return DraftsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - draftType: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}draft_type'])!, - existingId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}existing_id']), - replyId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}reply_id']), - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title']), - url: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}url']), - customThumbnail: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}custom_thumbnail']), - altText: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}alt_text']), - body: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}body']), - ); - } - - @override - Drafts createAlias(String alias) { - return Drafts(attachedDatabase, alias); - } -} - -class DraftsData extends DataClass implements Insertable { - final int id; - final String draftType; - final int? existingId; - final int? replyId; - final String? title; - final String? url; - final String? customThumbnail; - final String? altText; - final String? body; - const DraftsData( - {required this.id, - required this.draftType, - this.existingId, - this.replyId, - this.title, - this.url, - this.customThumbnail, - this.altText, - this.body}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['draft_type'] = Variable(draftType); - if (!nullToAbsent || existingId != null) { - map['existing_id'] = Variable(existingId); - } - if (!nullToAbsent || replyId != null) { - map['reply_id'] = Variable(replyId); - } - if (!nullToAbsent || title != null) { - map['title'] = Variable(title); - } - if (!nullToAbsent || url != null) { - map['url'] = Variable(url); - } - if (!nullToAbsent || customThumbnail != null) { - map['custom_thumbnail'] = Variable(customThumbnail); - } - if (!nullToAbsent || altText != null) { - map['alt_text'] = Variable(altText); - } - if (!nullToAbsent || body != null) { - map['body'] = Variable(body); - } - return map; - } - - DraftsCompanion toCompanion(bool nullToAbsent) { - return DraftsCompanion( - id: Value(id), - draftType: Value(draftType), - existingId: existingId == null && nullToAbsent - ? const Value.absent() - : Value(existingId), - replyId: replyId == null && nullToAbsent - ? const Value.absent() - : Value(replyId), - title: - title == null && nullToAbsent ? const Value.absent() : Value(title), - url: url == null && nullToAbsent ? const Value.absent() : Value(url), - customThumbnail: customThumbnail == null && nullToAbsent - ? const Value.absent() - : Value(customThumbnail), - altText: altText == null && nullToAbsent - ? const Value.absent() - : Value(altText), - body: body == null && nullToAbsent ? const Value.absent() : Value(body), - ); - } - - factory DraftsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return DraftsData( - id: serializer.fromJson(json['id']), - draftType: serializer.fromJson(json['draftType']), - existingId: serializer.fromJson(json['existingId']), - replyId: serializer.fromJson(json['replyId']), - title: serializer.fromJson(json['title']), - url: serializer.fromJson(json['url']), - customThumbnail: serializer.fromJson(json['customThumbnail']), - altText: serializer.fromJson(json['altText']), - body: serializer.fromJson(json['body']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'draftType': serializer.toJson(draftType), - 'existingId': serializer.toJson(existingId), - 'replyId': serializer.toJson(replyId), - 'title': serializer.toJson(title), - 'url': serializer.toJson(url), - 'customThumbnail': serializer.toJson(customThumbnail), - 'altText': serializer.toJson(altText), - 'body': serializer.toJson(body), - }; - } - - DraftsData copyWith( - {int? id, - String? draftType, - Value existingId = const Value.absent(), - Value replyId = const Value.absent(), - Value title = const Value.absent(), - Value url = const Value.absent(), - Value customThumbnail = const Value.absent(), - Value altText = const Value.absent(), - Value body = const Value.absent()}) => - DraftsData( - id: id ?? this.id, - draftType: draftType ?? this.draftType, - existingId: existingId.present ? existingId.value : this.existingId, - replyId: replyId.present ? replyId.value : this.replyId, - title: title.present ? title.value : this.title, - url: url.present ? url.value : this.url, - customThumbnail: customThumbnail.present - ? customThumbnail.value - : this.customThumbnail, - altText: altText.present ? altText.value : this.altText, - body: body.present ? body.value : this.body, - ); - DraftsData copyWithCompanion(DraftsCompanion data) { - return DraftsData( - id: data.id.present ? data.id.value : this.id, - draftType: data.draftType.present ? data.draftType.value : this.draftType, - existingId: - data.existingId.present ? data.existingId.value : this.existingId, - replyId: data.replyId.present ? data.replyId.value : this.replyId, - title: data.title.present ? data.title.value : this.title, - url: data.url.present ? data.url.value : this.url, - customThumbnail: data.customThumbnail.present - ? data.customThumbnail.value - : this.customThumbnail, - altText: data.altText.present ? data.altText.value : this.altText, - body: data.body.present ? data.body.value : this.body, - ); - } - - @override - String toString() { - return (StringBuffer('DraftsData(') - ..write('id: $id, ') - ..write('draftType: $draftType, ') - ..write('existingId: $existingId, ') - ..write('replyId: $replyId, ') - ..write('title: $title, ') - ..write('url: $url, ') - ..write('customThumbnail: $customThumbnail, ') - ..write('altText: $altText, ') - ..write('body: $body') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, draftType, existingId, replyId, title, - url, customThumbnail, altText, body); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is DraftsData && - other.id == this.id && - other.draftType == this.draftType && - other.existingId == this.existingId && - other.replyId == this.replyId && - other.title == this.title && - other.url == this.url && - other.customThumbnail == this.customThumbnail && - other.altText == this.altText && - other.body == this.body); -} - -class DraftsCompanion extends UpdateCompanion { - final Value id; - final Value draftType; - final Value existingId; - final Value replyId; - final Value title; - final Value url; - final Value customThumbnail; - final Value altText; - final Value body; - const DraftsCompanion({ - this.id = const Value.absent(), - this.draftType = const Value.absent(), - this.existingId = const Value.absent(), - this.replyId = const Value.absent(), - this.title = const Value.absent(), - this.url = const Value.absent(), - this.customThumbnail = const Value.absent(), - this.altText = const Value.absent(), - this.body = const Value.absent(), - }); - DraftsCompanion.insert({ - this.id = const Value.absent(), - required String draftType, - this.existingId = const Value.absent(), - this.replyId = const Value.absent(), - this.title = const Value.absent(), - this.url = const Value.absent(), - this.customThumbnail = const Value.absent(), - this.altText = const Value.absent(), - this.body = const Value.absent(), - }) : draftType = Value(draftType); - static Insertable custom({ - Expression? id, - Expression? draftType, - Expression? existingId, - Expression? replyId, - Expression? title, - Expression? url, - Expression? customThumbnail, - Expression? altText, - Expression? body, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (draftType != null) 'draft_type': draftType, - if (existingId != null) 'existing_id': existingId, - if (replyId != null) 'reply_id': replyId, - if (title != null) 'title': title, - if (url != null) 'url': url, - if (customThumbnail != null) 'custom_thumbnail': customThumbnail, - if (altText != null) 'alt_text': altText, - if (body != null) 'body': body, - }); - } - - DraftsCompanion copyWith( - {Value? id, - Value? draftType, - Value? existingId, - Value? replyId, - Value? title, - Value? url, - Value? customThumbnail, - Value? altText, - Value? body}) { - return DraftsCompanion( - id: id ?? this.id, - draftType: draftType ?? this.draftType, - existingId: existingId ?? this.existingId, - replyId: replyId ?? this.replyId, - title: title ?? this.title, - url: url ?? this.url, - customThumbnail: customThumbnail ?? this.customThumbnail, - altText: altText ?? this.altText, - body: body ?? this.body, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (draftType.present) { - map['draft_type'] = Variable(draftType.value); - } - if (existingId.present) { - map['existing_id'] = Variable(existingId.value); - } - if (replyId.present) { - map['reply_id'] = Variable(replyId.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (url.present) { - map['url'] = Variable(url.value); - } - if (customThumbnail.present) { - map['custom_thumbnail'] = Variable(customThumbnail.value); - } - if (altText.present) { - map['alt_text'] = Variable(altText.value); - } - if (body.present) { - map['body'] = Variable(body.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('DraftsCompanion(') - ..write('id: $id, ') - ..write('draftType: $draftType, ') - ..write('existingId: $existingId, ') - ..write('replyId: $replyId, ') - ..write('title: $title, ') - ..write('url: $url, ') - ..write('customThumbnail: $customThumbnail, ') - ..write('altText: $altText, ') - ..write('body: $body') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV7 extends GeneratedDatabase { - DatabaseAtV7(QueryExecutor e) : super(e); - late final Accounts accounts = Accounts(this); - late final Favorites favorites = Favorites(this); - late final LocalSubscriptions localSubscriptions = LocalSubscriptions(this); - late final UserLabels userLabels = UserLabels(this); - late final Drafts drafts = Drafts(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [accounts, favorites, localSubscriptions, userLabels, drafts]; - @override - int get schemaVersion => 7; -} diff --git a/test/drift/thunder/generated/schema_v8.dart b/test/drift/thunder/generated/schema_v8.dart deleted file mode 100644 index 0b5168996..000000000 --- a/test/drift/thunder/generated/schema_v8.dart +++ /dev/null @@ -1,1571 +0,0 @@ -// dart format width=80 -// GENERATED BY drift_dev, DO NOT MODIFY. -// ignore_for_file: type=lint,unused_import -// -import 'package:drift/drift.dart'; - -class Accounts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Accounts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT'); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - late final GeneratedColumn jwt = GeneratedColumn( - 'jwt', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - late final GeneratedColumn instance = GeneratedColumn( - 'instance', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - late final GeneratedColumn anonymous = GeneratedColumn( - 'anonymous', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL DEFAULT 0 CHECK (anonymous IN (0, 1))', - defaultValue: const CustomExpression('0')); - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, true, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - late final GeneratedColumn listIndex = GeneratedColumn( - 'list_index', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL DEFAULT (-1)', - defaultValue: const CustomExpression('-1')); - late final GeneratedColumn platform = GeneratedColumn( - 'platform', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - @override - List get $columns => - [id, username, jwt, instance, anonymous, userId, listIndex, platform]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'accounts'; - @override - Set get $primaryKey => {id}; - @override - AccountsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return AccountsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username']), - jwt: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}jwt']), - instance: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}instance']), - anonymous: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}anonymous'])!, - userId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}user_id']), - listIndex: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}list_index'])!, - platform: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}platform']), - ); - } - - @override - Accounts createAlias(String alias) { - return Accounts(attachedDatabase, alias); - } - - @override - bool get dontWriteConstraints => true; -} - -class AccountsData extends DataClass implements Insertable { - final int id; - final String? username; - final String? jwt; - final String? instance; - final int anonymous; - final int? userId; - final int listIndex; - final String? platform; - const AccountsData( - {required this.id, - this.username, - this.jwt, - this.instance, - required this.anonymous, - this.userId, - required this.listIndex, - this.platform}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - if (!nullToAbsent || username != null) { - map['username'] = Variable(username); - } - if (!nullToAbsent || jwt != null) { - map['jwt'] = Variable(jwt); - } - if (!nullToAbsent || instance != null) { - map['instance'] = Variable(instance); - } - map['anonymous'] = Variable(anonymous); - if (!nullToAbsent || userId != null) { - map['user_id'] = Variable(userId); - } - map['list_index'] = Variable(listIndex); - if (!nullToAbsent || platform != null) { - map['platform'] = Variable(platform); - } - return map; - } - - AccountsCompanion toCompanion(bool nullToAbsent) { - return AccountsCompanion( - id: Value(id), - username: username == null && nullToAbsent - ? const Value.absent() - : Value(username), - jwt: jwt == null && nullToAbsent ? const Value.absent() : Value(jwt), - instance: instance == null && nullToAbsent - ? const Value.absent() - : Value(instance), - anonymous: Value(anonymous), - userId: - userId == null && nullToAbsent ? const Value.absent() : Value(userId), - listIndex: Value(listIndex), - platform: platform == null && nullToAbsent - ? const Value.absent() - : Value(platform), - ); - } - - factory AccountsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return AccountsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - jwt: serializer.fromJson(json['jwt']), - instance: serializer.fromJson(json['instance']), - anonymous: serializer.fromJson(json['anonymous']), - userId: serializer.fromJson(json['userId']), - listIndex: serializer.fromJson(json['listIndex']), - platform: serializer.fromJson(json['platform']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'jwt': serializer.toJson(jwt), - 'instance': serializer.toJson(instance), - 'anonymous': serializer.toJson(anonymous), - 'userId': serializer.toJson(userId), - 'listIndex': serializer.toJson(listIndex), - 'platform': serializer.toJson(platform), - }; - } - - AccountsData copyWith( - {int? id, - Value username = const Value.absent(), - Value jwt = const Value.absent(), - Value instance = const Value.absent(), - int? anonymous, - Value userId = const Value.absent(), - int? listIndex, - Value platform = const Value.absent()}) => - AccountsData( - id: id ?? this.id, - username: username.present ? username.value : this.username, - jwt: jwt.present ? jwt.value : this.jwt, - instance: instance.present ? instance.value : this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId.present ? userId.value : this.userId, - listIndex: listIndex ?? this.listIndex, - platform: platform.present ? platform.value : this.platform, - ); - AccountsData copyWithCompanion(AccountsCompanion data) { - return AccountsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - jwt: data.jwt.present ? data.jwt.value : this.jwt, - instance: data.instance.present ? data.instance.value : this.instance, - anonymous: data.anonymous.present ? data.anonymous.value : this.anonymous, - userId: data.userId.present ? data.userId.value : this.userId, - listIndex: data.listIndex.present ? data.listIndex.value : this.listIndex, - platform: data.platform.present ? data.platform.value : this.platform, - ); - } - - @override - String toString() { - return (StringBuffer('AccountsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId, ') - ..write('listIndex: $listIndex, ') - ..write('platform: $platform') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash( - id, username, jwt, instance, anonymous, userId, listIndex, platform); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is AccountsData && - other.id == this.id && - other.username == this.username && - other.jwt == this.jwt && - other.instance == this.instance && - other.anonymous == this.anonymous && - other.userId == this.userId && - other.listIndex == this.listIndex && - other.platform == this.platform); -} - -class AccountsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value jwt; - final Value instance; - final Value anonymous; - final Value userId; - final Value listIndex; - final Value platform; - const AccountsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - this.listIndex = const Value.absent(), - this.platform = const Value.absent(), - }); - AccountsCompanion.insert({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.jwt = const Value.absent(), - this.instance = const Value.absent(), - this.anonymous = const Value.absent(), - this.userId = const Value.absent(), - this.listIndex = const Value.absent(), - this.platform = const Value.absent(), - }); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? jwt, - Expression? instance, - Expression? anonymous, - Expression? userId, - Expression? listIndex, - Expression? platform, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (jwt != null) 'jwt': jwt, - if (instance != null) 'instance': instance, - if (anonymous != null) 'anonymous': anonymous, - if (userId != null) 'user_id': userId, - if (listIndex != null) 'list_index': listIndex, - if (platform != null) 'platform': platform, - }); - } - - AccountsCompanion copyWith( - {Value? id, - Value? username, - Value? jwt, - Value? instance, - Value? anonymous, - Value? userId, - Value? listIndex, - Value? platform}) { - return AccountsCompanion( - id: id ?? this.id, - username: username ?? this.username, - jwt: jwt ?? this.jwt, - instance: instance ?? this.instance, - anonymous: anonymous ?? this.anonymous, - userId: userId ?? this.userId, - listIndex: listIndex ?? this.listIndex, - platform: platform ?? this.platform, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (jwt.present) { - map['jwt'] = Variable(jwt.value); - } - if (instance.present) { - map['instance'] = Variable(instance.value); - } - if (anonymous.present) { - map['anonymous'] = Variable(anonymous.value); - } - if (userId.present) { - map['user_id'] = Variable(userId.value); - } - if (listIndex.present) { - map['list_index'] = Variable(listIndex.value); - } - if (platform.present) { - map['platform'] = Variable(platform.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('AccountsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('jwt: $jwt, ') - ..write('instance: $instance, ') - ..write('anonymous: $anonymous, ') - ..write('userId: $userId, ') - ..write('listIndex: $listIndex, ') - ..write('platform: $platform') - ..write(')')) - .toString(); - } -} - -class Favorites extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Favorites(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT'); - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn communityId = GeneratedColumn( - 'community_id', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - @override - List get $columns => [id, accountId, communityId]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'favorites'; - @override - Set get $primaryKey => {id}; - @override - FavoritesData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return FavoritesData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - accountId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}account_id'])!, - communityId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}community_id'])!, - ); - } - - @override - Favorites createAlias(String alias) { - return Favorites(attachedDatabase, alias); - } - - @override - bool get dontWriteConstraints => true; -} - -class FavoritesData extends DataClass implements Insertable { - final int id; - final int accountId; - final int communityId; - const FavoritesData( - {required this.id, required this.accountId, required this.communityId}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['account_id'] = Variable(accountId); - map['community_id'] = Variable(communityId); - return map; - } - - FavoritesCompanion toCompanion(bool nullToAbsent) { - return FavoritesCompanion( - id: Value(id), - accountId: Value(accountId), - communityId: Value(communityId), - ); - } - - factory FavoritesData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return FavoritesData( - id: serializer.fromJson(json['id']), - accountId: serializer.fromJson(json['accountId']), - communityId: serializer.fromJson(json['communityId']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'accountId': serializer.toJson(accountId), - 'communityId': serializer.toJson(communityId), - }; - } - - FavoritesData copyWith({int? id, int? accountId, int? communityId}) => - FavoritesData( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - FavoritesData copyWithCompanion(FavoritesCompanion data) { - return FavoritesData( - id: data.id.present ? data.id.value : this.id, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - communityId: - data.communityId.present ? data.communityId.value : this.communityId, - ); - } - - @override - String toString() { - return (StringBuffer('FavoritesData(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, accountId, communityId); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is FavoritesData && - other.id == this.id && - other.accountId == this.accountId && - other.communityId == this.communityId); -} - -class FavoritesCompanion extends UpdateCompanion { - final Value id; - final Value accountId; - final Value communityId; - const FavoritesCompanion({ - this.id = const Value.absent(), - this.accountId = const Value.absent(), - this.communityId = const Value.absent(), - }); - FavoritesCompanion.insert({ - this.id = const Value.absent(), - required int accountId, - required int communityId, - }) : accountId = Value(accountId), - communityId = Value(communityId); - static Insertable custom({ - Expression? id, - Expression? accountId, - Expression? communityId, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (accountId != null) 'account_id': accountId, - if (communityId != null) 'community_id': communityId, - }); - } - - FavoritesCompanion copyWith( - {Value? id, Value? accountId, Value? communityId}) { - return FavoritesCompanion( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - communityId: communityId ?? this.communityId, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (communityId.present) { - map['community_id'] = Variable(communityId.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('FavoritesCompanion(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('communityId: $communityId') - ..write(')')) - .toString(); - } -} - -class LocalSubscriptions extends Table - with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - LocalSubscriptions(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT'); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn actorId = GeneratedColumn( - 'actor_id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn icon = GeneratedColumn( - 'icon', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - @override - List get $columns => [id, name, title, actorId, icon]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'local_subscriptions'; - @override - Set get $primaryKey => {id}; - @override - LocalSubscriptionsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return LocalSubscriptionsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title'])!, - actorId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}actor_id'])!, - icon: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}icon']), - ); - } - - @override - LocalSubscriptions createAlias(String alias) { - return LocalSubscriptions(attachedDatabase, alias); - } - - @override - bool get dontWriteConstraints => true; -} - -class LocalSubscriptionsData extends DataClass - implements Insertable { - final int id; - final String name; - final String title; - final String actorId; - final String? icon; - const LocalSubscriptionsData( - {required this.id, - required this.name, - required this.title, - required this.actorId, - this.icon}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - map['title'] = Variable(title); - map['actor_id'] = Variable(actorId); - if (!nullToAbsent || icon != null) { - map['icon'] = Variable(icon); - } - return map; - } - - LocalSubscriptionsCompanion toCompanion(bool nullToAbsent) { - return LocalSubscriptionsCompanion( - id: Value(id), - name: Value(name), - title: Value(title), - actorId: Value(actorId), - icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon), - ); - } - - factory LocalSubscriptionsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return LocalSubscriptionsData( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - title: serializer.fromJson(json['title']), - actorId: serializer.fromJson(json['actorId']), - icon: serializer.fromJson(json['icon']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'title': serializer.toJson(title), - 'actorId': serializer.toJson(actorId), - 'icon': serializer.toJson(icon), - }; - } - - LocalSubscriptionsData copyWith( - {int? id, - String? name, - String? title, - String? actorId, - Value icon = const Value.absent()}) => - LocalSubscriptionsData( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon.present ? icon.value : this.icon, - ); - LocalSubscriptionsData copyWithCompanion(LocalSubscriptionsCompanion data) { - return LocalSubscriptionsData( - id: data.id.present ? data.id.value : this.id, - name: data.name.present ? data.name.value : this.name, - title: data.title.present ? data.title.value : this.title, - actorId: data.actorId.present ? data.actorId.value : this.actorId, - icon: data.icon.present ? data.icon.value : this.icon, - ); - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsData(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, name, title, actorId, icon); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is LocalSubscriptionsData && - other.id == this.id && - other.name == this.name && - other.title == this.title && - other.actorId == this.actorId && - other.icon == this.icon); -} - -class LocalSubscriptionsCompanion - extends UpdateCompanion { - final Value id; - final Value name; - final Value title; - final Value actorId; - final Value icon; - const LocalSubscriptionsCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.title = const Value.absent(), - this.actorId = const Value.absent(), - this.icon = const Value.absent(), - }); - LocalSubscriptionsCompanion.insert({ - this.id = const Value.absent(), - required String name, - required String title, - required String actorId, - this.icon = const Value.absent(), - }) : name = Value(name), - title = Value(title), - actorId = Value(actorId); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? title, - Expression? actorId, - Expression? icon, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (title != null) 'title': title, - if (actorId != null) 'actor_id': actorId, - if (icon != null) 'icon': icon, - }); - } - - LocalSubscriptionsCompanion copyWith( - {Value? id, - Value? name, - Value? title, - Value? actorId, - Value? icon}) { - return LocalSubscriptionsCompanion( - id: id ?? this.id, - name: name ?? this.name, - title: title ?? this.title, - actorId: actorId ?? this.actorId, - icon: icon ?? this.icon, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (actorId.present) { - map['actor_id'] = Variable(actorId.value); - } - if (icon.present) { - map['icon'] = Variable(icon.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('LocalSubscriptionsCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('title: $title, ') - ..write('actorId: $actorId, ') - ..write('icon: $icon') - ..write(')')) - .toString(); - } -} - -class UserLabels extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - UserLabels(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT'); - late final GeneratedColumn username = GeneratedColumn( - 'username', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn label = GeneratedColumn( - 'label', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - @override - List get $columns => [id, username, label]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'user_labels'; - @override - Set get $primaryKey => {id}; - @override - UserLabelsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return UserLabelsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - username: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}username'])!, - label: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}label'])!, - ); - } - - @override - UserLabels createAlias(String alias) { - return UserLabels(attachedDatabase, alias); - } - - @override - bool get dontWriteConstraints => true; -} - -class UserLabelsData extends DataClass implements Insertable { - final int id; - final String username; - final String label; - const UserLabelsData( - {required this.id, required this.username, required this.label}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['username'] = Variable(username); - map['label'] = Variable(label); - return map; - } - - UserLabelsCompanion toCompanion(bool nullToAbsent) { - return UserLabelsCompanion( - id: Value(id), - username: Value(username), - label: Value(label), - ); - } - - factory UserLabelsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return UserLabelsData( - id: serializer.fromJson(json['id']), - username: serializer.fromJson(json['username']), - label: serializer.fromJson(json['label']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'username': serializer.toJson(username), - 'label': serializer.toJson(label), - }; - } - - UserLabelsData copyWith({int? id, String? username, String? label}) => - UserLabelsData( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - UserLabelsData copyWithCompanion(UserLabelsCompanion data) { - return UserLabelsData( - id: data.id.present ? data.id.value : this.id, - username: data.username.present ? data.username.value : this.username, - label: data.label.present ? data.label.value : this.label, - ); - } - - @override - String toString() { - return (StringBuffer('UserLabelsData(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, username, label); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is UserLabelsData && - other.id == this.id && - other.username == this.username && - other.label == this.label); -} - -class UserLabelsCompanion extends UpdateCompanion { - final Value id; - final Value username; - final Value label; - const UserLabelsCompanion({ - this.id = const Value.absent(), - this.username = const Value.absent(), - this.label = const Value.absent(), - }); - UserLabelsCompanion.insert({ - this.id = const Value.absent(), - required String username, - required String label, - }) : username = Value(username), - label = Value(label); - static Insertable custom({ - Expression? id, - Expression? username, - Expression? label, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (username != null) 'username': username, - if (label != null) 'label': label, - }); - } - - UserLabelsCompanion copyWith( - {Value? id, Value? username, Value? label}) { - return UserLabelsCompanion( - id: id ?? this.id, - username: username ?? this.username, - label: label ?? this.label, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (username.present) { - map['username'] = Variable(username.value); - } - if (label.present) { - map['label'] = Variable(label.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('UserLabelsCompanion(') - ..write('id: $id, ') - ..write('username: $username, ') - ..write('label: $label') - ..write(')')) - .toString(); - } -} - -class Drafts extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Drafts(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT'); - late final GeneratedColumn draftType = GeneratedColumn( - 'draft_type', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - late final GeneratedColumn existingId = GeneratedColumn( - 'existing_id', aliasedName, true, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - late final GeneratedColumn replyId = GeneratedColumn( - 'reply_id', aliasedName, true, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - late final GeneratedColumn active = GeneratedColumn( - 'active', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL DEFAULT 0 CHECK (active IN (0, 1))', - defaultValue: const CustomExpression('0')); - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - late final GeneratedColumn url = GeneratedColumn( - 'url', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - late final GeneratedColumn customThumbnail = GeneratedColumn( - 'custom_thumbnail', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - late final GeneratedColumn altText = GeneratedColumn( - 'alt_text', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - late final GeneratedColumn nsfw = GeneratedColumn( - 'nsfw', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NOT NULL DEFAULT 0 CHECK (nsfw IN (0, 1))', - defaultValue: const CustomExpression('0')); - late final GeneratedColumn languageId = GeneratedColumn( - 'language_id', aliasedName, true, - type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - late final GeneratedColumn body = GeneratedColumn( - 'body', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: 'NULL'); - @override - List get $columns => [ - id, - draftType, - existingId, - replyId, - active, - accountId, - title, - url, - customThumbnail, - altText, - nsfw, - languageId, - body - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'drafts'; - @override - Set get $primaryKey => {id}; - @override - DraftsData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return DraftsData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - draftType: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}draft_type'])!, - existingId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}existing_id']), - replyId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}reply_id']), - active: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}active'])!, - accountId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}account_id']), - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title']), - url: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}url']), - customThumbnail: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}custom_thumbnail']), - altText: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}alt_text']), - nsfw: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}nsfw'])!, - languageId: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}language_id']), - body: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}body']), - ); - } - - @override - Drafts createAlias(String alias) { - return Drafts(attachedDatabase, alias); - } - - @override - bool get dontWriteConstraints => true; -} - -class DraftsData extends DataClass implements Insertable { - final int id; - final String draftType; - final int? existingId; - final int? replyId; - final int active; - final String? accountId; - final String? title; - final String? url; - final String? customThumbnail; - final String? altText; - final int nsfw; - final int? languageId; - final String? body; - const DraftsData( - {required this.id, - required this.draftType, - this.existingId, - this.replyId, - required this.active, - this.accountId, - this.title, - this.url, - this.customThumbnail, - this.altText, - required this.nsfw, - this.languageId, - this.body}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['draft_type'] = Variable(draftType); - if (!nullToAbsent || existingId != null) { - map['existing_id'] = Variable(existingId); - } - if (!nullToAbsent || replyId != null) { - map['reply_id'] = Variable(replyId); - } - map['active'] = Variable(active); - if (!nullToAbsent || accountId != null) { - map['account_id'] = Variable(accountId); - } - if (!nullToAbsent || title != null) { - map['title'] = Variable(title); - } - if (!nullToAbsent || url != null) { - map['url'] = Variable(url); - } - if (!nullToAbsent || customThumbnail != null) { - map['custom_thumbnail'] = Variable(customThumbnail); - } - if (!nullToAbsent || altText != null) { - map['alt_text'] = Variable(altText); - } - map['nsfw'] = Variable(nsfw); - if (!nullToAbsent || languageId != null) { - map['language_id'] = Variable(languageId); - } - if (!nullToAbsent || body != null) { - map['body'] = Variable(body); - } - return map; - } - - DraftsCompanion toCompanion(bool nullToAbsent) { - return DraftsCompanion( - id: Value(id), - draftType: Value(draftType), - existingId: existingId == null && nullToAbsent - ? const Value.absent() - : Value(existingId), - replyId: replyId == null && nullToAbsent - ? const Value.absent() - : Value(replyId), - active: Value(active), - accountId: accountId == null && nullToAbsent - ? const Value.absent() - : Value(accountId), - title: - title == null && nullToAbsent ? const Value.absent() : Value(title), - url: url == null && nullToAbsent ? const Value.absent() : Value(url), - customThumbnail: customThumbnail == null && nullToAbsent - ? const Value.absent() - : Value(customThumbnail), - altText: altText == null && nullToAbsent - ? const Value.absent() - : Value(altText), - nsfw: Value(nsfw), - languageId: languageId == null && nullToAbsent - ? const Value.absent() - : Value(languageId), - body: body == null && nullToAbsent ? const Value.absent() : Value(body), - ); - } - - factory DraftsData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return DraftsData( - id: serializer.fromJson(json['id']), - draftType: serializer.fromJson(json['draftType']), - existingId: serializer.fromJson(json['existingId']), - replyId: serializer.fromJson(json['replyId']), - active: serializer.fromJson(json['active']), - accountId: serializer.fromJson(json['accountId']), - title: serializer.fromJson(json['title']), - url: serializer.fromJson(json['url']), - customThumbnail: serializer.fromJson(json['customThumbnail']), - altText: serializer.fromJson(json['altText']), - nsfw: serializer.fromJson(json['nsfw']), - languageId: serializer.fromJson(json['languageId']), - body: serializer.fromJson(json['body']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'draftType': serializer.toJson(draftType), - 'existingId': serializer.toJson(existingId), - 'replyId': serializer.toJson(replyId), - 'active': serializer.toJson(active), - 'accountId': serializer.toJson(accountId), - 'title': serializer.toJson(title), - 'url': serializer.toJson(url), - 'customThumbnail': serializer.toJson(customThumbnail), - 'altText': serializer.toJson(altText), - 'nsfw': serializer.toJson(nsfw), - 'languageId': serializer.toJson(languageId), - 'body': serializer.toJson(body), - }; - } - - DraftsData copyWith( - {int? id, - String? draftType, - Value existingId = const Value.absent(), - Value replyId = const Value.absent(), - int? active, - Value accountId = const Value.absent(), - Value title = const Value.absent(), - Value url = const Value.absent(), - Value customThumbnail = const Value.absent(), - Value altText = const Value.absent(), - int? nsfw, - Value languageId = const Value.absent(), - Value body = const Value.absent()}) => - DraftsData( - id: id ?? this.id, - draftType: draftType ?? this.draftType, - existingId: existingId.present ? existingId.value : this.existingId, - replyId: replyId.present ? replyId.value : this.replyId, - active: active ?? this.active, - accountId: accountId.present ? accountId.value : this.accountId, - title: title.present ? title.value : this.title, - url: url.present ? url.value : this.url, - customThumbnail: customThumbnail.present - ? customThumbnail.value - : this.customThumbnail, - altText: altText.present ? altText.value : this.altText, - nsfw: nsfw ?? this.nsfw, - languageId: languageId.present ? languageId.value : this.languageId, - body: body.present ? body.value : this.body, - ); - DraftsData copyWithCompanion(DraftsCompanion data) { - return DraftsData( - id: data.id.present ? data.id.value : this.id, - draftType: data.draftType.present ? data.draftType.value : this.draftType, - existingId: - data.existingId.present ? data.existingId.value : this.existingId, - replyId: data.replyId.present ? data.replyId.value : this.replyId, - active: data.active.present ? data.active.value : this.active, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - title: data.title.present ? data.title.value : this.title, - url: data.url.present ? data.url.value : this.url, - customThumbnail: data.customThumbnail.present - ? data.customThumbnail.value - : this.customThumbnail, - altText: data.altText.present ? data.altText.value : this.altText, - nsfw: data.nsfw.present ? data.nsfw.value : this.nsfw, - languageId: - data.languageId.present ? data.languageId.value : this.languageId, - body: data.body.present ? data.body.value : this.body, - ); - } - - @override - String toString() { - return (StringBuffer('DraftsData(') - ..write('id: $id, ') - ..write('draftType: $draftType, ') - ..write('existingId: $existingId, ') - ..write('replyId: $replyId, ') - ..write('active: $active, ') - ..write('accountId: $accountId, ') - ..write('title: $title, ') - ..write('url: $url, ') - ..write('customThumbnail: $customThumbnail, ') - ..write('altText: $altText, ') - ..write('nsfw: $nsfw, ') - ..write('languageId: $languageId, ') - ..write('body: $body') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, draftType, existingId, replyId, active, - accountId, title, url, customThumbnail, altText, nsfw, languageId, body); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is DraftsData && - other.id == this.id && - other.draftType == this.draftType && - other.existingId == this.existingId && - other.replyId == this.replyId && - other.active == this.active && - other.accountId == this.accountId && - other.title == this.title && - other.url == this.url && - other.customThumbnail == this.customThumbnail && - other.altText == this.altText && - other.nsfw == this.nsfw && - other.languageId == this.languageId && - other.body == this.body); -} - -class DraftsCompanion extends UpdateCompanion { - final Value id; - final Value draftType; - final Value existingId; - final Value replyId; - final Value active; - final Value accountId; - final Value title; - final Value url; - final Value customThumbnail; - final Value altText; - final Value nsfw; - final Value languageId; - final Value body; - const DraftsCompanion({ - this.id = const Value.absent(), - this.draftType = const Value.absent(), - this.existingId = const Value.absent(), - this.replyId = const Value.absent(), - this.active = const Value.absent(), - this.accountId = const Value.absent(), - this.title = const Value.absent(), - this.url = const Value.absent(), - this.customThumbnail = const Value.absent(), - this.altText = const Value.absent(), - this.nsfw = const Value.absent(), - this.languageId = const Value.absent(), - this.body = const Value.absent(), - }); - DraftsCompanion.insert({ - this.id = const Value.absent(), - required String draftType, - this.existingId = const Value.absent(), - this.replyId = const Value.absent(), - this.active = const Value.absent(), - this.accountId = const Value.absent(), - this.title = const Value.absent(), - this.url = const Value.absent(), - this.customThumbnail = const Value.absent(), - this.altText = const Value.absent(), - this.nsfw = const Value.absent(), - this.languageId = const Value.absent(), - this.body = const Value.absent(), - }) : draftType = Value(draftType); - static Insertable custom({ - Expression? id, - Expression? draftType, - Expression? existingId, - Expression? replyId, - Expression? active, - Expression? accountId, - Expression? title, - Expression? url, - Expression? customThumbnail, - Expression? altText, - Expression? nsfw, - Expression? languageId, - Expression? body, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (draftType != null) 'draft_type': draftType, - if (existingId != null) 'existing_id': existingId, - if (replyId != null) 'reply_id': replyId, - if (active != null) 'active': active, - if (accountId != null) 'account_id': accountId, - if (title != null) 'title': title, - if (url != null) 'url': url, - if (customThumbnail != null) 'custom_thumbnail': customThumbnail, - if (altText != null) 'alt_text': altText, - if (nsfw != null) 'nsfw': nsfw, - if (languageId != null) 'language_id': languageId, - if (body != null) 'body': body, - }); - } - - DraftsCompanion copyWith( - {Value? id, - Value? draftType, - Value? existingId, - Value? replyId, - Value? active, - Value? accountId, - Value? title, - Value? url, - Value? customThumbnail, - Value? altText, - Value? nsfw, - Value? languageId, - Value? body}) { - return DraftsCompanion( - id: id ?? this.id, - draftType: draftType ?? this.draftType, - existingId: existingId ?? this.existingId, - replyId: replyId ?? this.replyId, - active: active ?? this.active, - accountId: accountId ?? this.accountId, - title: title ?? this.title, - url: url ?? this.url, - customThumbnail: customThumbnail ?? this.customThumbnail, - altText: altText ?? this.altText, - nsfw: nsfw ?? this.nsfw, - languageId: languageId ?? this.languageId, - body: body ?? this.body, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (draftType.present) { - map['draft_type'] = Variable(draftType.value); - } - if (existingId.present) { - map['existing_id'] = Variable(existingId.value); - } - if (replyId.present) { - map['reply_id'] = Variable(replyId.value); - } - if (active.present) { - map['active'] = Variable(active.value); - } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (url.present) { - map['url'] = Variable(url.value); - } - if (customThumbnail.present) { - map['custom_thumbnail'] = Variable(customThumbnail.value); - } - if (altText.present) { - map['alt_text'] = Variable(altText.value); - } - if (nsfw.present) { - map['nsfw'] = Variable(nsfw.value); - } - if (languageId.present) { - map['language_id'] = Variable(languageId.value); - } - if (body.present) { - map['body'] = Variable(body.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('DraftsCompanion(') - ..write('id: $id, ') - ..write('draftType: $draftType, ') - ..write('existingId: $existingId, ') - ..write('replyId: $replyId, ') - ..write('active: $active, ') - ..write('accountId: $accountId, ') - ..write('title: $title, ') - ..write('url: $url, ') - ..write('customThumbnail: $customThumbnail, ') - ..write('altText: $altText, ') - ..write('nsfw: $nsfw, ') - ..write('languageId: $languageId, ') - ..write('body: $body') - ..write(')')) - .toString(); - } -} - -class DatabaseAtV8 extends GeneratedDatabase { - DatabaseAtV8(QueryExecutor e) : super(e); - late final Accounts accounts = Accounts(this); - late final Favorites favorites = Favorites(this); - late final LocalSubscriptions localSubscriptions = LocalSubscriptions(this); - late final UserLabels userLabels = UserLabels(this); - late final Drafts drafts = Drafts(this); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => - [accounts, favorites, localSubscriptions, userLabels, drafts]; - @override - int get schemaVersion => 8; -} diff --git a/test/drift/thunder/migration_test.dart b/test/drift/thunder/migration_test.dart deleted file mode 100644 index 4cb41c9af..000000000 --- a/test/drift/thunder/migration_test.dart +++ /dev/null @@ -1,173 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:drift_dev/api/migrations_native.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:thunder/src/foundation/persistence/database/database.dart'; - -import 'generated/schema.dart'; -import 'generated/schema_v3.dart' as v3; -import 'generated/schema_v4.dart' as v4; -import 'generated/schema_v5.dart' as v5; -import 'generated/schema_v6.dart' as v6; -import 'generated/schema_v7.dart' as v7; -import 'generated/schema_v8.dart' as v8; - -void main() { - driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; - late SchemaVerifier verifier; - - setUpAll(() => verifier = SchemaVerifier(GeneratedHelper())); - - group('general database migrations', () { - // These tests verify all possible schema updates with a simple (no data) migration. - // This is a quick way to ensure that written database migrations properly alter the schema. - const versions = GeneratedHelper.versions; - - for (final (i, fromVersion) in versions.indexed) { - group('from $fromVersion', () { - for (final toVersion in versions.skip(i + 1)) { - test('migration from v$fromVersion to v$toVersion', () async { - final schema = await verifier.schemaAt(fromVersion); - final db = AppDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, toVersion); - await db.close(); - }); - } - }); - } - }); - - group('database migrations with data integrity', () { - group('from v3 to v4', () { - test('add custom_thumbnail to Drafts table', () async { - // Add data to insert into the old database, and the expected rows after the migration. - final oldDraftsData = [v3.DraftsData(id: 1, draftType: 'postCreate', existingId: 1, replyId: 1, title: 'title', url: 'url', body: 'body')]; - final expectedNewDraftsData = [v4.DraftsData(id: 1, draftType: 'postCreate', existingId: 1, replyId: 1, title: 'title', url: 'url', body: 'body')]; - - await verifier.testWithDataIntegrity( - oldVersion: 3, - newVersion: 4, - createOld: v3.DatabaseAtV3.new, - createNew: v4.DatabaseAtV4.new, - openTestedDatabase: AppDatabase.new, - createItems: (batch, oldDb) => batch.insertAll(oldDb.drafts, oldDraftsData), - validateItems: (newDb) async => expect(expectedNewDraftsData, await newDb.select(newDb.drafts).get()), - ); - }); - }); - - group('from v4 to v5', () { - test('add list_index column and set list_index to id', () async { - // Add data to insert into the old database, and the expected rows after the migration. - final oldAccountsData = [ - v4.AccountsData(id: 1, username: 'thunder', jwt: 'jwt', instance: 'lemmy.thunderapp.dev', anonymous: false, userId: 1), - v4.AccountsData(id: 2, instance: 'lemmy.thunderapp.dev', anonymous: true), - ]; - - final expectedNewAccountsData = [ - v5.AccountsData(id: 1, username: 'thunder', jwt: 'jwt', instance: 'lemmy.thunderapp.dev', anonymous: false, userId: 1, listIndex: 1), - v5.AccountsData(id: 2, instance: 'lemmy.thunderapp.dev', anonymous: true, listIndex: 2), - ]; - - await verifier.testWithDataIntegrity( - oldVersion: 4, - newVersion: 5, - createOld: v4.DatabaseAtV4.new, - createNew: v5.DatabaseAtV5.new, - openTestedDatabase: AppDatabase.new, - createItems: (batch, oldDb) => batch.insertAll(oldDb.accounts, oldAccountsData), - validateItems: (newDb) async => expect(expectedNewAccountsData, await newDb.select(newDb.accounts).get()), - ); - }); - }); - - group('from v5 to v6', () { - test('add alt_text column to Drafts table', () async { - // Add data to insert into the old database, and the expected rows after the migration. - final oldDraftsData = [v5.DraftsData(id: 1, draftType: 'postCreate', existingId: 1, replyId: 1, title: 'title', url: 'url', body: 'body')]; - final expectedNewDraftsData = [v6.DraftsData(id: 1, draftType: 'postCreate', existingId: 1, replyId: 1, title: 'title', url: 'url', body: 'body')]; - - await verifier.testWithDataIntegrity( - oldVersion: 5, - newVersion: 6, - createOld: v5.DatabaseAtV5.new, - createNew: v6.DatabaseAtV6.new, - openTestedDatabase: AppDatabase.new, - createItems: (batch, oldDb) => batch.insertAll(oldDb.drafts, oldDraftsData), - validateItems: (newDb) async => expect(expectedNewDraftsData, await newDb.select(newDb.drafts).get()), - ); - }); - }); - - group('from v6 to v7', () { - test('add platform column to Accounts table and set platform to lemmy', () async { - // Add data to insert into the old database, and the expected rows after the migration. - final oldAccountsData = [ - v6.AccountsData(id: 1, username: 'thunder', jwt: 'jwt', instance: 'lemmy.thunderapp.dev', anonymous: false, userId: 1, listIndex: 1), - v6.AccountsData(id: 2, instance: 'lemmy.thunderapp.dev', anonymous: true, listIndex: 2), - ]; - - final expectedNewAccountsData = [ - v7.AccountsData(id: 1, username: 'thunder', jwt: 'jwt', instance: 'lemmy.thunderapp.dev', anonymous: false, userId: 1, listIndex: 1, platform: 'lemmy'), - v7.AccountsData(id: 2, instance: 'lemmy.thunderapp.dev', anonymous: true, listIndex: 2, platform: 'lemmy'), - ]; - - await verifier.testWithDataIntegrity( - oldVersion: 6, - newVersion: 7, - createOld: v6.DatabaseAtV6.new, - createNew: v7.DatabaseAtV7.new, - openTestedDatabase: AppDatabase.new, - createItems: (batch, oldDb) => batch.insertAll(oldDb.accounts, oldAccountsData), - validateItems: (newDb) async => expect(expectedNewAccountsData, await newDb.select(newDb.accounts).get()), - ); - }); - }); - - group('from v7 to v8', () { - test('add active/account_id/nsfw/language_id columns to Drafts table', () async { - final oldDraftsData = [ - v7.DraftsData( - id: 1, - draftType: 'postCreate', - existingId: null, - replyId: 10, - title: 'title', - url: 'url', - customThumbnail: 'thumbnail', - altText: 'alt', - body: 'body', - ) - ]; - - final expectedNewDraftsData = [ - v8.DraftsData( - id: 1, - draftType: 'postCreate', - existingId: null, - replyId: 10, - active: 0, - accountId: null, - title: 'title', - url: 'url', - customThumbnail: 'thumbnail', - altText: 'alt', - nsfw: 0, - languageId: null, - body: 'body', - ) - ]; - - await verifier.testWithDataIntegrity( - oldVersion: 7, - newVersion: 8, - createOld: v7.DatabaseAtV7.new, - createNew: v8.DatabaseAtV8.new, - openTestedDatabase: AppDatabase.new, - createItems: (batch, oldDb) => batch.insertAll(oldDb.drafts, oldDraftsData), - validateItems: (newDb) async => expect(expectedNewDraftsData, await newDb.select(newDb.drafts).get()), - ); - }); - }); - }); -} diff --git a/test/features/account/profile_community_usecase_test.dart b/test/features/account/profile_community_usecase_test.dart deleted file mode 100644 index f3710ee82..000000000 --- a/test/features/account/profile_community_usecase_test.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:thunder/src/features/account/account.dart'; -import 'package:thunder/src/features/account/domain/utils/profile_community_utils.dart'; -import 'package:thunder/src/features/community/community.dart'; -import 'package:thunder/src/features/user/user.dart'; - -class _MockThunderUser extends Mock implements ThunderUser {} - -ThunderCommunity _community(int id) { - return ThunderCommunity( - id: id, - name: 'c$id', - title: 'c$id', - removed: false, - published: DateTime.utc(2024, 1, 1), - deleted: false, - nsfw: false, - actorId: 'https://lemmy.world/c/c$id', - local: true, - hidden: false, - postingRestrictedToMods: false, - instanceId: 1, - visibility: 'Public', - ); -} - -void main() { - group('ProfileCommunityUsecase', () { - test('isSameUser returns true when user id matches account userId', () { - final user = _MockThunderUser(); - when(() => user.id).thenReturn(42); - - const account = Account( - id: '1', - index: 0, - instance: 'lemmy.world', - userId: 42, - ); - - expect( - isSameUser(user: user, account: account), - isTrue, - ); - }); - - test('filterFavorites keeps only subscribed favorite communities', () { - final subscriptions = [_community(1), _community(2), _community(3)]; - const favorites = [ - Favorite(id: 'a', communityId: 2, accountId: '1'), - Favorite(id: 'b', communityId: 4, accountId: '1'), - ]; - - final result = filterFavorites( - subscriptions: subscriptions, - favorites: favorites, - ); - - expect(result.map((community) => community.id), [2]); - }); - }); -} diff --git a/test/features/comment/comment_node_test.dart b/test/features/comment/comment_node_test.dart deleted file mode 100644 index 38adb7aa1..000000000 --- a/test/features/comment/comment_node_test.dart +++ /dev/null @@ -1,273 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:thunder/src/features/comment/comment.dart'; - -void main() { - group('Comment Node', () { - ThunderComment createMockComment({ - required int id, - required String path, - int creatorId = 1, - int postId = 1, - String content = 'Test', - bool removed = false, - DateTime? published, - bool deleted = false, - String apId = 'https://example.com/comment/1', - bool local = true, - bool distinguished = false, - int languageId = 0, - }) { - return ThunderComment( - id: id, - creatorId: creatorId, - postId: postId, - content: content, - removed: removed, - published: published ?? DateTime.now(), - deleted: deleted, - apId: apId, - local: local, - path: path, - distinguished: distinguished, - languageId: languageId, - ); - } - - group('constructor', () { - test('constructor initializes with correct values', () { - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - - expect(node.comment, equals(comment)); - expect(node.replies, isEmpty); - }); - }); - - group('depth', () { - test('depth returns 0 for root comment', () { - final root = CommentNode(); - expect(root.depth, equals(0)); - }); - - test('depth returns correct depth for comment', () { - final root = CommentNode(); - - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - root.insert(node); - - expect(node.depth, equals(0)); - }); - - test('depth returns correct depth for deeply nested comment', () { - final root = CommentNode(); - - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - root.insert(node); - - final nestedComment = createMockComment(id: 2, path: '0.1.2'); - final nestedNode = CommentNode(comment: nestedComment); - node.insert(nestedNode); - - expect(nestedNode.depth, equals(1)); - }); - }); - - group('parent', () { - test('parent returns null for root comment', () { - final root = CommentNode(); - expect(root.parent, isNull); - }); - - test('parent returns correct parent for comment', () { - final root = CommentNode(); - - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - root.insert(node); - - expect(node.parent, equals('0')); - }); - - test('parent returns correct parent for nested comment', () { - final root = CommentNode(); - - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - root.insert(node); - - final nestedComment = createMockComment(id: 2, path: '0.1.2'); - final nestedNode = CommentNode(comment: nestedComment); - node.insert(nestedNode); - - expect(nestedNode.parent, equals('0.1')); - }); - }); - - group('insert', () { - test('insert adds comment to replies', () { - final root = CommentNode(); - - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - root.insert(node); - - expect(root.replies.length, equals(1)); - expect(root.replies[0].comment, equals(comment)); - }); - - test( - 'insert does not add comment if it is not a direct child of this comment', - () { - final root = CommentNode(); - - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - root.insert(node); - - final reply = createMockComment(id: 2, path: '0.2'); - final replyNode = CommentNode(comment: reply); - node.insert(replyNode); - - expect(node.replies.length, equals(0)); - }); - - test('insert does not add comment if it has the same id as this comment', - () { - final root = CommentNode(); - - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - root.insert(node); - - final reply = createMockComment(id: 1, path: '0.1.2'); - final replyNode = CommentNode(comment: reply); - node.insert(replyNode); - - final sameIdComment = createMockComment(id: 1, path: '0.1'); - final sameIdNode = CommentNode(comment: sameIdComment); - node.insert(sameIdNode); - - expect(node.replies.length, equals(1)); - expect(node.replies[0].comment, equals(reply)); - }); - - test('insert replaces comment if it already exists', () { - final root = CommentNode(); - - final comment = - createMockComment(id: 1, path: '0.1', content: 'Original'); - final node = CommentNode(comment: comment); - root.insert(node); - - final updatedComment = - createMockComment(id: 1, path: '0.1', content: 'Updated'); - final updatedNode = CommentNode(comment: updatedComment); - root.insert(updatedNode); - - expect(root.replies.length, equals(1)); - expect(root.replies[0].comment, equals(updatedComment)); - }); - }); - - group('search', () { - test('search finds comment by id', () { - final root = CommentNode(); - - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - root.insert(node); - - expect(root.search(1), equals(node)); - }); - - test('search finds comment by id in nested replies', () { - final root = CommentNode(); - - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - root.insert(node); - - final nestedComment = createMockComment(id: 2, path: '0.1.2'); - final nestedNode = CommentNode(comment: nestedComment); - node.insert(nestedNode); - - expect(root.search(2), equals(nestedNode)); - }); - - test('search returns null if comment is not found', () { - final root = CommentNode(); - - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - root.insert(node); - - expect(root.search(2), isNull); - }); - }); - - group('flatten', () { - test('flatten returns a list of comments', () { - final root = CommentNode(); - - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - root.insert(node); - - final flattened = root.flatten(); - expect(flattened.length, equals(1)); - expect(flattened[0].comment, equals(comment)); - }); - - test('flatten returns a list of comments with nested replies', () { - final root = CommentNode(); - - final comment = createMockComment(id: 1, path: '0.1'); - final node = CommentNode(comment: comment); - root.insert(node); - - final nestedComment = createMockComment(id: 2, path: '0.1.2'); - final nestedNode = CommentNode(comment: nestedComment); - node.insert(nestedNode); - - final flattened = root.flatten(); - expect(flattened.length, equals(2)); - expect(flattened[0].comment, equals(comment)); - expect(flattened[1].comment, equals(nestedComment)); - }); - - test('flatten returns a list of comments in the correct order', () { - final root = CommentNode(); - - final comment1 = createMockComment(id: 1, path: '0.1'); - final comment2 = createMockComment(id: 2, path: '0.2'); - final node1 = CommentNode(comment: comment1); - final node2 = CommentNode(comment: comment2); - - root.insert(node1); - root.insert(node2); - - final nestedComment1 = createMockComment(id: 8, path: '0.1.8'); - final nestedComment2 = createMockComment(id: 3, path: '0.1.3'); - final nestedComment3 = createMockComment(id: 4, path: '0.2.1'); - final nestedNode1 = CommentNode(comment: nestedComment1); - final nestedNode2 = CommentNode(comment: nestedComment2); - final nestedNode3 = CommentNode(comment: nestedComment3); - - node1.insert(nestedNode1); - node1.insert(nestedNode2); - node2.insert(nestedNode3); - - final flattened = root.flatten(); - expect(flattened.length, equals(5)); - expect(flattened[0].comment, equals(comment1)); - expect(flattened[1].comment, equals(nestedComment1)); - expect(flattened[2].comment, equals(nestedComment2)); - expect(flattened[3].comment, equals(comment2)); - expect(flattened[4].comment, equals(nestedComment3)); - }); - }); - }); -} diff --git a/test/features/comment/create_comment_cubit_test.dart b/test/features/comment/create_comment_cubit_test.dart deleted file mode 100644 index fd475f766..000000000 --- a/test/features/comment/create_comment_cubit_test.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/src/foundation/contracts/localization_service.dart'; -import 'package:thunder/src/foundation/errors/app_error_reason.dart'; -import 'package:thunder/src/features/account/account.dart'; -import 'package:thunder/src/features/comment/comment.dart'; - -class _MockCommentRepository extends Mock implements CommentRepository {} - -class _MockAccountRepository extends Mock implements AccountRepository {} - -class _MockThunderComment extends Mock implements ThunderComment {} - -class _MockLocalizationService extends Mock implements LocalizationService {} - -class _MockAppLocalizations extends Mock implements AppLocalizations {} - -void main() { - group('CreateCommentCubit', () { - late _MockCommentRepository commentRepository; - late _MockAccountRepository accountRepository; - late _MockLocalizationService localizationService; - late _MockAppLocalizations l10n; - - setUp(() { - commentRepository = _MockCommentRepository(); - accountRepository = _MockAccountRepository(); - localizationService = _MockLocalizationService(); - l10n = _MockAppLocalizations(); - when(() => localizationService.l10n).thenReturn(l10n); - when(() => l10n.userNotLoggedIn).thenReturn('User not logged in'); - }); - - blocTest( - 'emits not-logged-in typed error for anonymous image upload', - build: () => CreateCommentCubit( - account: const Account( - id: 'anon', - index: 0, - instance: 'lemmy.world', - anonymous: true, - ), - commentRepositoryFactory: (_) => commentRepository, - accountRepositoryFactory: (_) => accountRepository, - localizationService: localizationService, - ), - act: (cubit) => cubit.uploadImages(const ['a.png']), - expect: () => [ - isA() - .having((state) => state.status, 'status', - CreateCommentStatus.imageUploadFailure) - .having((state) => state.errorReason?.category, 'error category', - AppErrorCategory.notLoggedIn), - ], - ); - - blocTest( - 'emits typed actionFailed error when comment create fails', - build: () { - when(() => commentRepository.create( - postId: 12, - content: 'Hello', - parentId: null, - languageId: null, - )).thenThrow(Exception('failed')); - - return CreateCommentCubit( - account: const Account(id: '1', index: 0, instance: 'lemmy.world'), - commentRepositoryFactory: (_) => commentRepository, - accountRepositoryFactory: (_) => accountRepository, - localizationService: localizationService, - ); - }, - act: (cubit) => cubit.createOrEditComment(postId: 12, content: 'Hello'), - expect: () => [ - isA().having( - (state) => state.status, 'status', CreateCommentStatus.submitting), - isA() - .having((state) => state.status, 'status', CreateCommentStatus.error) - .having((state) => state.errorReason?.category, 'error category', - AppErrorCategory.actionFailed), - ], - ); - - blocTest( - 'emits success when comment create succeeds', - build: () { - final comment = _MockThunderComment(); - when(() => comment.id).thenReturn(88); - - when(() => commentRepository.create( - postId: 12, - content: 'Hello', - parentId: null, - languageId: null, - )).thenAnswer((_) async => comment); - - return CreateCommentCubit( - account: const Account(id: '1', index: 0, instance: 'lemmy.world'), - commentRepositoryFactory: (_) => commentRepository, - accountRepositoryFactory: (_) => accountRepository, - localizationService: localizationService, - ); - }, - act: (cubit) => cubit.createOrEditComment(postId: 12, content: 'Hello'), - expect: () => [ - isA().having( - (state) => state.status, 'status', CreateCommentStatus.submitting), - isA() - .having((state) => state.status, 'status', CreateCommentStatus.success) - .having((state) => state.errorReason, 'errorReason', isNull), - ], - ); - }); -} diff --git a/test/features/drafts/draft_model_test.dart b/test/features/drafts/draft_model_test.dart deleted file mode 100644 index 1f1420a80..000000000 --- a/test/features/drafts/draft_model_test.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'package:drift/native.dart'; - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:thunder/src/features/drafts/drafts.dart'; -import 'package:thunder/src/foundation/persistence/persistence.dart'; - -void main() { - late AppDatabase appDatabase; - late DraftRepository repository; - - setUp(() { - appDatabase = AppDatabase(NativeDatabase.memory()); - database = appDatabase; - repository = DraftRepositoryImpl(database: appDatabase); - }); - - tearDown(() async { - await appDatabase.close(); - }); - - test('upsert draft sets active and stores account', () async { - final draft = Draft( - id: '', - draftType: DraftType.postCreate, - replyId: 11, - title: 'title', - body: 'body', - accountId: '2', - nsfw: true, - languageId: 4, - ); - - final saved = await repository.upsertDraft(draft, active: true); - final active = await repository.fetchActiveDraft(); - - expect(saved, isNotNull); - expect(active, isNotNull); - expect(active!.active, isTrue); - expect(active.draftType, DraftType.postCreate); - expect(active.replyId, 11); - expect(active.accountId, '2'); - expect(active.nsfw, isTrue); - expect(active.languageId, 4); - }); - - test('setting active draft clears previously active draft', () async { - await repository.upsertDraft( - Draft(id: '', draftType: DraftType.postCreate, replyId: 11, title: 'one', accountId: '1'), - active: true, - ); - - await repository.upsertDraft( - Draft(id: '', draftType: DraftType.commentCreateFromPost, replyId: 42, body: 'two', accountId: '3'), - active: true, - ); - - final active = await repository.fetchActiveDraft(); - final previous = await repository.fetchDraft(DraftType.postCreate, null, 11); - - expect(active, isNotNull); - expect(active!.draftType, DraftType.commentCreateFromPost); - expect(active.replyId, 42); - expect(previous, isNotNull); - expect(previous!.active, isFalse); - }); - - test('upsert stores comment language id', () async { - await repository.upsertDraft( - Draft(id: '', draftType: DraftType.commentCreateFromPost, replyId: 777, body: 'with language', accountId: '1', languageId: 19), - active: true, - ); - - final fetched = await repository.fetchDraft(DraftType.commentCreateFromPost, null, 777); - - expect(fetched, isNotNull); - expect(fetched!.languageId, 19); - }); - - test('upsert keeps draft identity while account changes', () async { - final initial = await repository.upsertDraft( - Draft(id: '', draftType: DraftType.postCreate, replyId: 10, title: 'hello', accountId: '1'), - active: true, - ); - - final updated = await repository.upsertDraft( - Draft(id: '', draftType: DraftType.postCreate, replyId: 10, title: 'hello', accountId: '999'), - active: true, - ); - - final fetched = await repository.fetchDraft(DraftType.postCreate, null, 10); - - expect(initial, isNotNull); - expect(updated, isNotNull); - expect(updated!.id, initial!.id); - expect(fetched, isNotNull); - expect(fetched!.accountId, '999'); - }); - - test('fetch supports legacy commentCreate type', () async { - await repository.upsertDraft( - Draft(id: '', draftType: DraftType.commentCreate, replyId: 99, body: 'legacy', accountId: '1'), - active: true, - ); - - final restoredFromPost = await repository.fetchDraft(DraftType.commentCreateFromPost, null, 99); - final restoredFromComment = await repository.fetchDraft(DraftType.commentCreateFromComment, null, 99); - - expect(restoredFromPost, isNotNull); - expect(restoredFromComment, isNotNull); - expect(restoredFromPost!.body, 'legacy'); - expect(restoredFromComment!.body, 'legacy'); - }); - - test('fetch supports legacy postCreateGeneral type', () async { - await repository.upsertDraft( - Draft(id: '', draftType: DraftType.postCreateGeneral, title: 'legacy post create general', accountId: '1'), - active: true, - ); - - final restored = await repository.fetchDraft(DraftType.postCreate, null, null); - - expect(restored, isNotNull); - expect(restored!.title, 'legacy post create general'); - }); - - test('database enforces one globally active draft', () async { - final first = await repository.upsertDraft( - Draft(id: '', draftType: DraftType.postCreate, replyId: 1, title: 'one', accountId: '1'), - ); - final second = await repository.upsertDraft( - Draft(id: '', draftType: DraftType.postCreate, replyId: 2, title: 'two', accountId: '1'), - ); - - expect(first, isNotNull); - expect(second, isNotNull); - - await database.customStatement('UPDATE drafts SET active = 1 WHERE id = ${first!.id}'); - - expect( - () => database.customStatement('UPDATE drafts SET active = 1 WHERE id = ${second!.id}'), - throwsA(isA()), - ); - }); -} diff --git a/test/features/drafts/draft_utils_test.dart b/test/features/drafts/draft_utils_test.dart deleted file mode 100644 index 8ffd873fb..000000000 --- a/test/features/drafts/draft_utils_test.dart +++ /dev/null @@ -1,194 +0,0 @@ -import 'package:drift/native.dart'; - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:thunder/src/features/drafts/drafts.dart'; -import 'package:thunder/src/foundation/persistence/persistence.dart'; -import 'package:thunder/src/foundation/primitives/enums/subscription_status.dart'; -import 'package:thunder/src/foundation/primitives/models/thunder_community.dart'; -import 'package:thunder/src/foundation/primitives/models/thunder_comment.dart'; -import 'package:thunder/src/foundation/primitives/models/thunder_post.dart'; -import 'package:thunder/src/foundation/primitives/models/thunder_user.dart'; - -void main() { - group('draft compose context helpers', () { - test('resolve post context handles edit and create', () { - final editContext = resolvePostDraftContext(editingPostId: 4, communityId: 11); - final createContext = resolvePostDraftContext(editingPostId: null, communityId: 11); - - expect(editContext.draftType, DraftType.postEdit); - expect(editContext.existingId, 4); - expect(editContext.replyId, isNull); - - expect(createContext.draftType, DraftType.postCreate); - expect(createContext.existingId, isNull); - expect(createContext.replyId, 11); - }); - - test('resolve comment context handles comment edit and reply targets', () { - final editContext = resolveCommentDraftContext(editingCommentId: 9, postId: 21, parentCommentId: 33); - final replyToPostContext = resolveCommentDraftContext(editingCommentId: null, postId: 21, parentCommentId: null); - final replyToCommentContext = resolveCommentDraftContext(editingCommentId: null, postId: 21, parentCommentId: 33); - - expect(editContext.draftType, DraftType.commentEdit); - expect(editContext.existingId, 9); - - expect(replyToPostContext.draftType, DraftType.commentCreateFromPost); - expect(replyToPostContext.replyId, 21); - - expect(replyToCommentContext.draftType, DraftType.commentCreateFromComment); - expect(replyToCommentContext.replyId, 33); - }); - }); - - group('persist compose draft', () { - late AppDatabase appDatabase; - late DraftRepository draftRepository; - - setUp(() { - appDatabase = AppDatabase(NativeDatabase.memory()); - database = appDatabase; - draftRepository = DraftRepositoryImpl(database: appDatabase); - }); - - tearDown(() async { - await appDatabase.close(); - }); - - test('persists active draft when content exists', () async { - final context = resolvePostDraftContext(editingPostId: null, communityId: 42); - final draft = buildPostDraft( - context: context, - accountId: '1', - title: 'title', - url: '', - customThumbnail: '', - altText: '', - nsfw: true, - languageId: 3, - body: 'body', - ); - - final result = await persistDraft( - repository: draftRepository, - context: context, - draft: draft, - save: true, - differsFromEdit: true, - hasContent: draft.isPostNotEmpty, - ); - - final active = await draftRepository.fetchActiveDraft(); - - expect(result, DraftPersistenceResult.saved); - expect(active, isNotNull); - expect(active!.active, isTrue); - expect(active.replyId, 42); - }); - - test('skips when comment context is missing reply target', () async { - const context = DraftContext(draftType: DraftType.commentCreateFromPost); - final draft = buildCommentDraft(context: context, accountId: '1', languageId: 5, body: 'hello'); - - final result = await persistDraft( - repository: draftRepository, - context: context, - draft: draft, - save: true, - differsFromEdit: true, - hasContent: draft.isCommentNotEmpty, - ); - - final active = await draftRepository.fetchActiveDraft(); - expect(result, DraftPersistenceResult.skipped); - expect(active, isNull); - }); - }); - - group('draft diff helpers', () { - test('post draft diff includes nsfw and language', () { - final draft = Draft( - id: '', - draftType: DraftType.postEdit, - existingId: 10, - title: 'title', - url: 'https://example.com', - customThumbnail: 'https://example.com/image.png', - altText: 'alt', - nsfw: true, - languageId: 2, - body: 'body', - ); - - final post = ThunderPost( - id: 10, - name: 'title', - url: 'https://example.com', - body: 'body', - altText: 'alt', - creatorId: 1, - communityId: 2, - removed: false, - locked: false, - published: DateTime.now(), - deleted: false, - nsfw: false, - thumbnailUrl: 'https://example.com/image.png', - apId: 'apId', - local: true, - languageId: 1, - featuredCommunity: false, - featuredLocal: false, - creator: ThunderUser( - id: 1, - name: 'user', - banned: false, - published: DateTime.fromMillisecondsSinceEpoch(0), - actorId: 'actor', - local: true, - deleted: false, - botAccount: false, - instanceId: 1, - ), - community: ThunderCommunity( - id: 2, - name: 'community', - title: 'community', - removed: false, - published: DateTime.fromMillisecondsSinceEpoch(0), - deleted: false, - nsfw: false, - actorId: 'communityActor', - local: true, - hidden: false, - postingRestrictedToMods: false, - instanceId: 1, - visibility: 'Public', - subscribed: SubscriptionStatus.notSubscribed, - ), - ); - - expect(postDraftDiffersFromEdit(draft, post), isTrue); - }); - - test('comment draft diff includes language', () { - final draft = Draft(id: '', draftType: DraftType.commentEdit, existingId: 5, body: 'body', languageId: 2); - final comment = ThunderComment( - id: 5, - creatorId: 1, - postId: 2, - content: 'body', - removed: false, - published: DateTime.fromMillisecondsSinceEpoch(0), - deleted: false, - apId: 'apId', - local: true, - path: '0.5', - distinguished: false, - languageId: 1, - ); - - expect(commentDraftDiffersFromEdit(draft, comment), isTrue); - }); - }); -} diff --git a/test/features/feed/feed_state_test.dart b/test/features/feed/feed_state_test.dart deleted file mode 100644 index aed0f79a1..000000000 --- a/test/features/feed/feed_state_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:thunder/src/features/feed/presentation/state/feed_bloc.dart'; - -void main() { - test('FeedState.copyWith preserves excessiveApiCalls by default', () { - const state = FeedState(excessiveApiCalls: true); - - final updated = state.copyWith(status: FeedStatus.success); - - expect(updated.excessiveApiCalls, isTrue); - }); -} diff --git a/test/features/feed/feed_view_usecase_test.dart b/test/features/feed/feed_view_usecase_test.dart deleted file mode 100644 index 759d1e958..000000000 --- a/test/features/feed/feed_view_usecase_test.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:thunder/src/features/feed/domain/utils/feed_collection_utils.dart'; -import 'package:thunder/src/features/post/post.dart'; - -ThunderPost _post(int id) { - return ThunderPost( - id: id, - name: 'post-$id', - creatorId: 1, - communityId: 1, - removed: false, - locked: false, - published: DateTime(2024, 1, 1), - deleted: false, - nsfw: false, - apId: 'https://example.com/post/$id', - local: true, - languageId: 0, - featuredCommunity: false, - featuredLocal: false, - ); -} - -void main() { - group('FeedViewUsecase', () { - test('hidePostsByIds removes only matching post IDs', () { - final posts = [ - _post(1), - _post(2), - _post(3), - ]; - - final filtered = hidePostsByIds( - posts: posts, - postIds: {2, 99}, - ); - - expect(filtered.map((p) => p.id), [1, 3]); - expect(posts.map((p) => p.id), [1, 2, 3]); - }); - }); -} diff --git a/test/features/inbox/inbox_bloc_test.dart b/test/features/inbox/inbox_bloc_test.dart deleted file mode 100644 index d6942a6fc..000000000 --- a/test/features/inbox/inbox_bloc_test.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:thunder/src/foundation/contracts/localization_service.dart'; -import 'package:thunder/src/foundation/primitives/enums/comment_sort_type.dart'; -import 'package:thunder/src/features/account/account.dart'; -import 'package:thunder/src/features/comment/comment.dart'; -import 'package:thunder/src/features/inbox/inbox.dart'; -import 'package:thunder/src/features/notification/notification.dart'; - -class _MockCommentRepository extends Mock implements CommentRepository {} - -class _MockNotificationRepository extends Mock - implements NotificationRepository {} - -void main() { - group('InboxBloc', () { - late _MockCommentRepository commentRepository; - late _MockNotificationRepository notificationRepository; - - setUp(() { - commentRepository = _MockCommentRepository(); - notificationRepository = _MockNotificationRepository(); - }); - - blocTest( - 'increments only mention page on mention reset fetch', - build: () { - when(() => notificationRepository.mentions( - unread: true, - limit: 20, - sort: CommentSortType.new_, - page: 1)).thenAnswer((_) async => []); - when(() => notificationRepository.unreadNotificationsCount()) - .thenAnswer((_) async => const UnreadNotificationsCount( - privateMessages: 0, - mentions: 0, - replies: 0, - )); - - return InboxBloc( - account: const Account(id: '1', index: 0, instance: 'example.com'), - commentRepository: commentRepository, - notificationRepository: notificationRepository, - localizationService: const GlobalContextLocalizationService(), - ); - }, - act: (bloc) => bloc - .add(const GetInboxEvent(reset: true, inboxType: InboxType.mentions)), - expect: () => [ - isA() - .having((state) => state.status, 'status', InboxStatus.loading), - isA() - .having((state) => state.status, 'status', InboxStatus.success) - .having((state) => state.inboxMentionPage, 'mention page', 2) - .having((state) => state.inboxReplyPage, 'reply page', 1) - .having((state) => state.inboxPrivateMessagePage, - 'private message page', 1), - ], - verify: (_) { - verify(() => notificationRepository.mentions( - unread: true, - limit: 20, - sort: CommentSortType.new_, - page: 1)).called(1); - }, - ); - }); -} diff --git a/test/features/inbox/inbox_cleanup_usecase_test.dart b/test/features/inbox/inbox_cleanup_usecase_test.dart deleted file mode 100644 index 343b61dca..000000000 --- a/test/features/inbox/inbox_cleanup_usecase_test.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:thunder/src/foundation/primitives/models/thunder_private_message.dart'; -import 'package:thunder/src/features/comment/comment.dart'; -import 'package:thunder/src/features/inbox/domain/utils/inbox_utils.dart'; - -ThunderComment _comment({ - required int id, - required String content, - required bool removed, - required bool deleted, -}) { - return ThunderComment( - id: id, - creatorId: 1, - postId: 1, - content: content, - removed: removed, - published: DateTime(2024, 1, 1), - deleted: deleted, - apId: 'https://example.com/comment/$id', - local: true, - path: '0.$id', - distinguished: false, - languageId: 0, - ); -} - -void main() { - group('InboxCleanupUsecase', () { - test('cleans deleted private messages', () { - final messages = [ - ThunderPrivateMessage( - id: 1, - content: 'hello', - deleted: false, - read: false, - published: DateTime(2024, 1, 1), - ), - ThunderPrivateMessage( - id: 2, - content: 'bye', - deleted: true, - read: false, - published: DateTime(2024, 1, 1), - ), - ]; - - final cleaned = cleanDeletedMessages(messages); - expect(cleaned.first.content, 'hello'); - expect(cleaned.last.content, '_deleted by creator_'); - }); - - test('cleans removed and deleted mentions', () { - final mentions = [ - _comment(id: 1, content: 'normal', removed: false, deleted: false), - _comment(id: 2, content: 'removed', removed: true, deleted: false), - _comment(id: 3, content: 'deleted', removed: false, deleted: true), - ]; - - final cleaned = cleanDeletedMentions(mentions); - expect(cleaned[0].content, 'normal'); - expect(cleaned[1].content, '_deleted by moderator_'); - expect(cleaned[2].content, '_deleted by creator_'); - }); - }); -} diff --git a/test/features/instance/instance_pagination_usecase_test.dart b/test/features/instance/instance_pagination_usecase_test.dart deleted file mode 100644 index 076254eaa..000000000 --- a/test/features/instance/instance_pagination_usecase_test.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:thunder/src/features/instance/domain/utils/instance_utils.dart'; - -void main() { - group('InstancePaginationUsecase', () { - test('shouldSkipFetch skips non-reset requests while already loading', () { - expect( -shouldSkipFetch( - currentlyLoading: true, - page: 2, - ), - isTrue, - ); - expect( -shouldSkipFetch( - currentlyLoading: true, - page: 1, - ), - isFalse, - ); - }); - - test('previousItemsForPage resets on first page and preserves otherwise', () { - expect( -previousItemsForPage( - page: 1, - currentItems: const [1, 2], - ), - isEmpty, - ); - - expect( -previousItemsForPage( - page: 3, - currentItems: const [1, 2], - ), - [1, 2], - ); - }); - - test('hasReachedEnd matches fetch count boundaries', () { - expect( -hasReachedEnd( - fetchedCount: 0, - pageLimit: 30, - ), - isTrue, - ); - expect( -hasReachedEnd( - fetchedCount: 10, - pageLimit: 30, - ), - isTrue, - ); - expect( -hasReachedEnd( - fetchedCount: 30, - pageLimit: 30, - ), - isFalse, - ); - }); - }); -} diff --git a/test/features/instance/instance_resolution_usecase_test.dart b/test/features/instance/instance_resolution_usecase_test.dart deleted file mode 100644 index 7a095e948..000000000 --- a/test/features/instance/instance_resolution_usecase_test.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:thunder/src/features/instance/domain/utils/instance_utils.dart'; - -void main() { - group('InstanceResolutionUsecase', () { - test('resolveInBatches aggregates non-null resolved values', () async { - final snapshots = >[]; - - final resolved = await resolveInBatches( - source: const [1, 2, 3, 4, 5], - batchSize: 2, - resolver: (value) async => value.isEven ? value * 10 : null, - onBatchResolved: (items) => snapshots.add(items), - ); - - expect(resolved, [20, 40]); - expect(snapshots, [ - [20], - [20, 40], - [20, 40], - ]); - }); - }); -} diff --git a/test/features/moderator/report_feed_usecase_test.dart b/test/features/moderator/report_feed_usecase_test.dart deleted file mode 100644 index b4f1b1055..000000000 --- a/test/features/moderator/report_feed_usecase_test.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:thunder/src/foundation/primitives/models/thunder_comment_report.dart'; -import 'package:thunder/src/foundation/primitives/models/thunder_post_report.dart'; -import 'package:thunder/src/features/moderator/domain/utils/report_utils.dart'; - -class _MockPostReport extends Mock implements ThunderPostReport {} - -class _MockCommentReport extends Mock implements ThunderCommentReport {} - -void main() { - group('ReportFeedUsecase', () { - test('shouldSkipPagination respects fetching and feed-end flags', () { - expect( - shouldSkipPagination( - isFetching: true, - hasReachedPostReportsEnd: false, - hasReachedCommentReportsEnd: false, - isPostFeed: true, - ), - isTrue, - ); - - expect( - shouldSkipPagination( - isFetching: false, - hasReachedPostReportsEnd: true, - hasReachedCommentReportsEnd: false, - isPostFeed: true, - ), - isTrue, - ); - - expect( - shouldSkipPagination( - isFetching: false, - hasReachedPostReportsEnd: false, - hasReachedCommentReportsEnd: true, - isPostFeed: false, - ), - isTrue, - ); - - expect( - shouldSkipPagination( - isFetching: false, - hasReachedPostReportsEnd: false, - hasReachedCommentReportsEnd: false, - isPostFeed: true, - ), - isFalse, - ); - }); - - test('append helpers preserve existing order and append incoming items', - () { - final postA = _MockPostReport(); - final postB = _MockPostReport(); - final commentA = _MockCommentReport(); - final commentB = _MockCommentReport(); - - final mergedPosts = appendPostReports( - current: [postA], - incoming: [postB], - ); - final mergedComments = appendCommentReports( - current: [commentA], - incoming: [commentB], - ); - - expect(mergedPosts, [postA, postB]); - expect(mergedComments, [commentA, commentB]); - }); - }); -} diff --git a/test/features/modlog/modlog_state_test.dart b/test/features/modlog/modlog_state_test.dart deleted file mode 100644 index 0d2c0af96..000000000 --- a/test/features/modlog/modlog_state_test.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:thunder/src/foundation/errors/app_error_reason.dart'; -import 'package:thunder/src/features/modlog/modlog.dart'; - -void main() { - group('ModlogState.errorReason', () { - test('returns null when message is null', () { - const state = ModlogState(); - expect(state.errorReason, isNull); - }); - - test('returns typed unexpected reason when message exists', () { - const state = ModlogState( - status: ModlogStatus.failure, - message: 'failed to fetch modlog', - ); - - expect(state.errorReason?.category, AppErrorCategory.unexpected); - expect(state.errorReason?.message, 'failed to fetch modlog'); - }); - }); -} diff --git a/test/features/post/collapsed_comments_usecase_test.dart b/test/features/post/collapsed_comments_usecase_test.dart deleted file mode 100644 index 2c9fb7c39..000000000 --- a/test/features/post/collapsed_comments_usecase_test.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:thunder/src/features/post/domain/utils/comment_state_utils.dart'; - -void main() { - group('CollapsedCommentsUsecase', () { - test('adds comment id when collapsed is true', () { - final updated = update( - current: const [1, 2], - commentId: 3, - collapsed: true, - ); - - expect(updated, [1, 2, 3]); - }); - - test('removes comment id when collapsed is false', () { - final updated = update( - current: const [1, 2, 3], - commentId: 2, - collapsed: false, - ); - - expect(updated, [1, 3]); - }); - }); -} diff --git a/test/features/post/create_post_cubit_test.dart b/test/features/post/create_post_cubit_test.dart deleted file mode 100644 index 5c35b0066..000000000 --- a/test/features/post/create_post_cubit_test.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/src/foundation/contracts/localization_service.dart'; -import 'package:thunder/src/foundation/errors/app_error_reason.dart'; -import 'package:thunder/src/features/account/account.dart'; -import 'package:thunder/src/features/post/post.dart'; - -class _MockPostRepository extends Mock implements PostRepository {} - -class _MockAccountRepository extends Mock implements AccountRepository {} - -class _MockThunderPost extends Mock implements ThunderPost {} - -class _MockLocalizationService extends Mock implements LocalizationService {} - -class _MockAppLocalizations extends Mock implements AppLocalizations {} - -void main() { - group('CreatePostCubit', () { - late _MockPostRepository postRepository; - late _MockAccountRepository accountRepository; - late _MockLocalizationService localizationService; - late _MockAppLocalizations l10n; - - setUp(() { - postRepository = _MockPostRepository(); - accountRepository = _MockAccountRepository(); - localizationService = _MockLocalizationService(); - l10n = _MockAppLocalizations(); - when(() => localizationService.l10n).thenReturn(l10n); - when(() => l10n.userNotLoggedIn).thenReturn('User not logged in'); - }); - - blocTest( - 'emits not-logged-in typed error for anonymous image upload', - build: () => CreatePostCubit( - account: const Account( - id: 'anon', - index: 0, - instance: 'lemmy.world', - anonymous: true, - ), - postRepositoryFactory: (_) => postRepository, - accountRepositoryFactory: (_) => accountRepository, - localizationService: localizationService, - ), - act: (cubit) => cubit.uploadImages(const ['a.png']), - expect: () => [ - isA() - .having((state) => state.status, 'status', - CreatePostStatus.imageUploadFailure) - .having((state) => state.errorReason?.category, 'error category', - AppErrorCategory.notLoggedIn), - ], - ); - - blocTest( - 'emits typed actionFailed error when create fails', - build: () { - when(() => postRepository.create( - communityId: 1, - name: 'Title', - body: null, - url: null, - customThumbnail: null, - altText: null, - nsfw: null, - postIdBeingEdited: null, - languageId: null, - )).thenThrow(Exception('failed')); - - return CreatePostCubit( - account: const Account(id: '1', index: 0, instance: 'lemmy.world'), - postRepositoryFactory: (_) => postRepository, - accountRepositoryFactory: (_) => accountRepository, - localizationService: localizationService, - ); - }, - act: (cubit) => cubit.createOrEditPost(communityId: 1, name: 'Title'), - expect: () => [ - isA().having( - (state) => state.status, 'status', CreatePostStatus.submitting), - isA() - .having((state) => state.status, 'status', CreatePostStatus.error) - .having((state) => state.errorReason?.category, 'error category', - AppErrorCategory.actionFailed), - ], - ); - - blocTest( - 'emits success when create succeeds', - build: () { - final post = _MockThunderPost(); - when(() => post.id).thenReturn(101); - - when(() => postRepository.create( - communityId: 1, - name: 'Title', - body: null, - url: null, - customThumbnail: null, - altText: null, - nsfw: null, - postIdBeingEdited: null, - languageId: null, - )).thenAnswer((_) async => post); - - return CreatePostCubit( - account: const Account(id: '1', index: 0, instance: 'lemmy.world'), - postRepositoryFactory: (_) => postRepository, - accountRepositoryFactory: (_) => accountRepository, - localizationService: localizationService, - ); - }, - act: (cubit) => cubit.createOrEditPost(communityId: 1, name: 'Title'), - expect: () => [ - isA().having( - (state) => state.status, 'status', CreatePostStatus.submitting), - isA() - .having((state) => state.status, 'status', CreatePostStatus.success) - .having((state) => state.errorReason, 'errorReason', isNull), - ], - ); - }); -} diff --git a/test/features/post/post_navigation_state_test.dart b/test/features/post/post_navigation_state_test.dart deleted file mode 100644 index d2599f78a..000000000 --- a/test/features/post/post_navigation_state_test.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:thunder/src/features/post/presentation/state/post_navigation_cubit/post_navigation_cubit.dart'; - -void main() { - test( - 'PostNavigationState.copyWith preserves highlightedCommentId when omitted', - () { - const state = PostNavigationState(highlightedCommentId: 42); - - final updated = state.copyWith(navigateCommentIndex: 5); - - expect(updated.highlightedCommentId, 42); - expect(updated.navigateCommentIndex, 5); - }); -} diff --git a/test/features/search/search_bloc_test.dart b/test/features/search/search_bloc_test.dart deleted file mode 100644 index 3a2278e33..000000000 --- a/test/features/search/search_bloc_test.dart +++ /dev/null @@ -1,283 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'package:thunder/src/features/account/account.dart'; -import 'package:thunder/src/features/comment/comment.dart'; -import 'package:thunder/src/features/community/community.dart'; -import 'package:thunder/src/features/instance/instance.dart'; -import 'package:thunder/src/features/search/search.dart'; -import 'package:thunder/src/features/user/user.dart'; -import 'package:thunder/src/foundation/persistence/persistence.dart'; -import 'package:thunder/src/foundation/primitives/primitives.dart'; - -class _MockCommentRepository extends Mock implements CommentRepository {} - -class _MockSearchRepository extends Mock implements SearchRepository {} - -class _MockCommunityRepository extends Mock implements CommunityRepository {} - -class _MockUserRepository extends Mock implements UserRepository {} - -class _MockInstanceRepository extends Mock implements InstanceRepository {} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - const account = Account(id: '1', index: 0, instance: 'lemmy.world'); - const communityId = 42; - - setUpAll(() async { - SharedPreferences.setMockInitialValues({}); - await UserPreferences.instance.initialize(); - }); - - group('SearchBloc', () { - late _MockCommentRepository commentRepository; - late _MockSearchRepository searchRepository; - late _MockCommunityRepository communityRepository; - late _MockUserRepository userRepository; - late _MockInstanceRepository instanceRepository; - - setUp(() { - commentRepository = _MockCommentRepository(); - searchRepository = _MockSearchRepository(); - communityRepository = _MockCommunityRepository(); - userRepository = _MockUserRepository(); - instanceRepository = _MockInstanceRepository(); - }); - - blocTest( - 'passes community filter when searching posts', - build: () { - when( - () => searchRepository.search( - query: 'thunder', - type: MetaSearchType.posts, - sort: SearchSortType.topYear, - listingType: FeedListType.all, - limit: searchResultsPerPage, - page: 1, - communityId: communityId, - creatorId: null, - minimumUpvotes: null, - nsfw: null, - ), - ).thenAnswer( - (_) async => const SearchResults( - type: MetaSearchType.posts, - comments: [], - posts: [], - communities: [], - users: [], - ), - ); - - return SearchBloc( - account: account, - commentRepository: commentRepository, - searchRepository: searchRepository, - communityRepository: communityRepository, - userRepository: userRepository, - instanceRepository: instanceRepository, - ); - }, - act: (bloc) { - bloc.add(const SearchFiltersUpdated(searchType: MetaSearchType.posts, communityFilter: communityId)); - bloc.add(const SearchStarted(query: 'thunder')); - }, - expect: () => [ - isA().having((state) => state.searchType, 'searchType', MetaSearchType.posts).having((state) => state.communityFilter, 'communityFilter', communityId), - isA().having((state) => state.status, 'status', SearchStatus.loading), - isA().having((state) => state.status, 'status', SearchStatus.success).having((state) => state.communityFilter, 'communityFilter', communityId), - ], - verify: (_) { - verify( - () => searchRepository.search( - query: 'thunder', - type: MetaSearchType.posts, - sort: SearchSortType.topYear, - listingType: FeedListType.all, - limit: searchResultsPerPage, - page: 1, - communityId: communityId, - creatorId: null, - minimumUpvotes: null, - nsfw: null, - ), - ).called(1); - }, - ); - - blocTest( - 'passes community filter when searching comments', - build: () { - when( - () => searchRepository.search( - query: 'thunder', - type: MetaSearchType.comments, - sort: SearchSortType.topYear, - listingType: FeedListType.all, - limit: searchResultsPerPage, - page: 1, - communityId: communityId, - creatorId: null, - minimumUpvotes: null, - nsfw: null, - ), - ).thenAnswer( - (_) async => const SearchResults( - type: MetaSearchType.comments, - comments: [], - posts: [], - communities: [], - users: [], - ), - ); - - return SearchBloc( - account: account, - commentRepository: commentRepository, - searchRepository: searchRepository, - communityRepository: communityRepository, - userRepository: userRepository, - instanceRepository: instanceRepository, - ); - }, - act: (bloc) { - bloc.add(const SearchFiltersUpdated(searchType: MetaSearchType.comments, communityFilter: communityId)); - bloc.add(const SearchStarted(query: 'thunder')); - }, - expect: () => [ - isA().having((state) => state.searchType, 'searchType', MetaSearchType.comments).having((state) => state.communityFilter, 'communityFilter', communityId), - isA().having((state) => state.status, 'status', SearchStatus.loading), - isA().having((state) => state.status, 'status', SearchStatus.success).having((state) => state.communityFilter, 'communityFilter', communityId), - ], - verify: (_) { - verify( - () => searchRepository.search( - query: 'thunder', - type: MetaSearchType.comments, - sort: SearchSortType.topYear, - listingType: FeedListType.all, - limit: searchResultsPerPage, - page: 1, - communityId: communityId, - creatorId: null, - minimumUpvotes: null, - nsfw: null, - ), - ).called(1); - }, - ); - - blocTest( - 'keeps community filter when switching from posts to comments', - build: () { - when( - () => searchRepository.search( - query: 'post query', - type: MetaSearchType.posts, - sort: SearchSortType.topYear, - listingType: FeedListType.all, - limit: searchResultsPerPage, - page: 1, - communityId: communityId, - creatorId: null, - minimumUpvotes: null, - nsfw: null, - ), - ).thenAnswer( - (_) async => const SearchResults( - type: MetaSearchType.posts, - comments: [], - posts: [], - communities: [], - users: [], - ), - ); - - when( - () => searchRepository.search( - query: 'comment query', - type: MetaSearchType.comments, - sort: SearchSortType.topYear, - listingType: FeedListType.all, - limit: searchResultsPerPage, - page: 1, - communityId: communityId, - creatorId: null, - minimumUpvotes: null, - nsfw: null, - ), - ).thenAnswer( - (_) async => const SearchResults( - type: MetaSearchType.comments, - comments: [], - posts: [], - communities: [], - users: [], - ), - ); - - return SearchBloc( - account: account, - commentRepository: commentRepository, - searchRepository: searchRepository, - communityRepository: communityRepository, - userRepository: userRepository, - instanceRepository: instanceRepository, - ); - }, - act: (bloc) async { - bloc.add(const SearchFiltersUpdated(searchType: MetaSearchType.posts, communityFilter: communityId)); - bloc.add(const SearchStarted(query: 'post query')); - - await Future.delayed(Duration.zero); - - bloc.add(const SearchFiltersUpdated(searchType: MetaSearchType.comments)); - bloc.add(const SearchStarted(query: 'comment query')); - }, - expect: () => [ - isA().having((state) => state.searchType, 'searchType', MetaSearchType.posts).having((state) => state.communityFilter, 'communityFilter', communityId), - isA().having((state) => state.status, 'status', SearchStatus.loading), - isA().having((state) => state.status, 'status', SearchStatus.success).having((state) => state.communityFilter, 'communityFilter', communityId), - isA().having((state) => state.searchType, 'searchType', MetaSearchType.comments).having((state) => state.communityFilter, 'communityFilter', communityId), - isA().having((state) => state.status, 'status', SearchStatus.loading), - isA().having((state) => state.status, 'status', SearchStatus.success).having((state) => state.communityFilter, 'communityFilter', communityId), - ], - verify: (_) { - verify( - () => searchRepository.search( - query: 'post query', - type: MetaSearchType.posts, - sort: SearchSortType.topYear, - listingType: FeedListType.all, - limit: searchResultsPerPage, - page: 1, - communityId: communityId, - creatorId: null, - minimumUpvotes: null, - nsfw: null, - ), - ).called(1); - - verify( - () => searchRepository.search( - query: 'comment query', - type: MetaSearchType.comments, - sort: SearchSortType.topYear, - listingType: FeedListType.all, - limit: searchResultsPerPage, - page: 1, - communityId: communityId, - creatorId: null, - minimumUpvotes: null, - nsfw: null, - ), - ).called(1); - }, - ); - }); -} diff --git a/test/features/user/user_media_usecase_test.dart b/test/features/user/user_media_usecase_test.dart deleted file mode 100644 index 6fb4e59f4..000000000 --- a/test/features/user/user_media_usecase_test.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:thunder/src/features/post/post.dart'; -import 'package:thunder/src/features/user/domain/utils/user_media_utils.dart'; - -ThunderPost _post(int id) { - return ThunderPost( - id: id, - name: 'post-$id', - creatorId: 1, - communityId: 1, - removed: false, - locked: false, - published: DateTime(2024, 1, 1), - deleted: false, - nsfw: false, - apId: 'https://example.com/post/$id', - local: true, - languageId: 0, - featuredCommunity: false, - featuredLocal: false, - ); -} - -void main() { - group('UserMediaUsecase', () { - test('removes images by alias', () { - final images = [ - { - 'local_image': {'pictrs_alias': 'a'}, - }, - { - 'local_image': {'pictrs_alias': 'b'}, - }, - ]; - - final updated = removeImageByAlias( - images: images, - alias: 'b', - ); - - expect(updated.length, 1); - expect(updated.first['local_image']['pictrs_alias'], 'a'); - }); - - test('merges posts without duplicates by ID', () { - final merged = mergeUniquePosts( - primary: [_post(1), _post(2)], - secondary: [_post(2), _post(3)], - ); - - expect(merged.map((post) => post.id), [1, 2, 3]); - }); - }); -} diff --git a/test/helpers/fake_preferences_store.dart b/test/helpers/fake_preferences_store.dart deleted file mode 100644 index 30d6316ec..000000000 --- a/test/helpers/fake_preferences_store.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:thunder/src/foundation/contracts/preferences_store.dart'; -import 'package:thunder/src/foundation/primitives/enums/local_settings.dart'; - -class FakePreferencesStore implements PreferencesStore { - FakePreferencesStore({Map? settings}) - : _settings = Map.from(settings ?? const {}); - - final Map _settings; - final Map _strings = {}; - - @override - T? getLocalSetting(LocalSettings setting) { - return _settings[setting] as T?; - } - - @override - void setSetting(LocalSettings setting, Object value) { - _settings[setting] = value; - } - - @override - void removeSetting(LocalSettings setting) { - _settings.remove(setting); - } - - @override - String? getString(String key) { - return _strings[key]; - } - - @override - Future setString(String key, String value) async { - _strings[key] = value; - return true; - } - - @override - Future remove(String key) async { - _strings.remove(key); - return true; - } -} diff --git a/test/utils/link_utils_test.dart b/test/utils/link_utils_test.dart deleted file mode 100644 index 944877ac4..000000000 --- a/test/utils/link_utils_test.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:thunder/src/foundation/primitives/models/parsed_link.dart'; -import 'package:thunder/src/foundation/utils/threadiverse_link_parser_utils.dart'; - -void main() { - group('ParsedLink', () { - test('qualified returns value@instance', () { - const link = ParsedLink(value: 'news', instance: 'lemmy.world'); - expect(link.qualified, 'news@lemmy.world'); - }); - - test('equality works correctly', () { - const link1 = ParsedLink(value: 'news', instance: 'lemmy.world'); - const link2 = ParsedLink(value: 'news', instance: 'lemmy.world'); - const link3 = ParsedLink(value: 'other', instance: 'lemmy.world'); - expect(link1, equals(link2)); - expect(link1, isNot(equals(link3))); - }); - }); - - group('Lemmy Community Parsing', () { - test('parses full community URL with federation', () { - final result = parseLemmyCommunity('https://lemmy.world/c/news@lemmy.ml'); - expect(result, isNotNull); - expect(result!.value, 'news'); - expect(result.instance, 'lemmy.ml'); - }); - - test('parses short community URL', () { - final result = parseLemmyCommunity('https://lemmy.world/c/news'); - expect(result, isNotNull); - expect(result!.value, 'news'); - expect(result.instance, 'lemmy.world'); - }); - - test('parses community mention with !', () { - final result = parseLemmyCommunity('!news@lemmy.world'); - expect(result, isNotNull); - expect(result!.value, 'news'); - expect(result.instance, 'lemmy.world'); - }); - - test('returns null for user URLs', () { - expect(parseLemmyCommunity('https://lemmy.world/u/darklightxi'), isNull); - expect(parseLemmyCommunity('https://lemmy.world/u/darklightxi@lemmy.ca'), - isNull); - }); - - test('returns null for @ mentions (users)', () { - expect(parseLemmyCommunity('@darklightxi@lemmy.world'), isNull); - }); - - test('returns null for PieFed post URLs (/c/community/p/postId)', () { - expect( - parseLemmyCommunity( - 'https://piefed.social/c/thunder_app/p/1422697/thunder-release-v0-8-0-initial-piefed-support'), - isNull); - expect( - parseLemmyCommunity('https://piefed.social/c/thunder_app/p/1422697'), - isNull); - }); - }); - - group('Lemmy User Parsing', () { - test('parses full user URL with federation', () { - final result = - parseLemmyUser('https://lemmy.world/u/darklightxi@lemmy.ca'); - expect(result, isNotNull); - expect(result!.value, 'darklightxi'); - expect(result.instance, 'lemmy.ca'); - }); - - test('parses short user URL', () { - final result = parseLemmyUser('https://lemmy.world/u/darklightxi'); - expect(result, isNotNull); - expect(result!.value, 'darklightxi'); - expect(result.instance, 'lemmy.world'); - }); - - test('parses user mention with @', () { - final result = parseLemmyUser('@darklightxi@lemmy.world'); - expect(result, isNotNull); - expect(result!.value, 'darklightxi'); - expect(result.instance, 'lemmy.world'); - }); - - test('returns null for community URLs', () { - expect(parseLemmyUser('https://lemmy.world/c/news'), isNull); - expect(parseLemmyUser('https://lemmy.world/c/news@lemmy.ml'), isNull); - }); - - test('returns null for ! mentions (communities)', () { - expect(parseLemmyUser('!news@lemmy.world'), isNull); - }); - }); - - group('Lemmy Post Parsing', () { - test('parses post URL', () { - final result = parseLemmyPostId('https://lemmy.world/post/12345'); - expect(result, isNotNull); - expect(result!.value, '12345'); - expect(result.instance, 'lemmy.world'); - }); - - test('returns null for PieFed comment URLs', () { - expect(parseLemmyPostId('https://piefed.social/post/123/comment/456'), - isNull); - }); - - test('returns null for Lemmy new format comment URLs', () { - expect(parseLemmyPostId('https://lemmy.world/post/123/456'), isNull); - }); - }); - - group('Lemmy Comment Parsing', () { - test('parses legacy comment URL', () { - final result = parseLemmyCommentId('https://lemmy.world/comment/12345'); - expect(result, isNotNull); - expect(result!.value, '12345'); - expect(result.instance, 'lemmy.world'); - }); - - test('parses new format comment URL (/post/123/456)', () { - final result = parseLemmyCommentId('https://lemmy.world/post/123/456'); - expect(result, isNotNull); - expect(result!.value, '456'); - expect(result.instance, 'lemmy.world'); - }); - - test('returns null for pure post URLs', () { - expect(parseLemmyCommentId('https://lemmy.world/post/123'), isNull); - }); - }); - - group('PieFed Comment Parsing', () { - test('parses PieFed comment URL (/post/123/comment/456)', () { - final result = parsePiefedCommentId( - 'https://piefed.social/post/1663157/comment/9679172'); - expect(result, isNotNull); - expect(result!.value, '9679172'); - expect(result.instance, 'piefed.social'); - }); - - test('returns null for Lemmy format comment URLs', () { - expect(parsePiefedCommentId('https://lemmy.world/comment/123'), isNull); - expect(parsePiefedCommentId('https://lemmy.world/post/123/456'), isNull); - }); - }); - - group('Unified Parsers', () { - group('parseCommunity', () { - test('parses Lemmy community URL', () { - final result = parseCommunity('https://lemmy.world/c/news'); - expect(result?.qualified, 'news@lemmy.world'); - }); - - test('parses PieFed community URL', () { - final result = - parseCommunity('https://piefed.social/c/news@lemmy.world'); - expect(result?.qualified, 'news@lemmy.world'); - }); - }); - - group('parseUser', () { - test('parses Lemmy user URL', () { - final result = parseUser('https://lemmy.world/u/darklightxi'); - expect(result?.qualified, 'darklightxi@lemmy.world'); - }); - - test('parses PieFed user URL', () { - final result = - parseUser('https://piefed.social/u/darklightxi@lemmy.ca'); - expect(result?.qualified, 'darklightxi@lemmy.ca'); - }); - }); - - group('parsePostId', () { - test('parses Lemmy post URL', () { - final result = parsePostId('https://lemmy.world/post/12345'); - expect(result?.value, '12345'); - expect(result?.instance, 'lemmy.world'); - }); - - test('parses PieFed post URL', () { - final result = parsePostId('https://piefed.social/post/1656906'); - expect(result?.value, '1656906'); - expect(result?.instance, 'piefed.social'); - }); - - test('returns null for comment URLs', () { - expect( - parsePostId('https://piefed.social/post/123/comment/456'), isNull); - expect(parsePostId('https://lemmy.world/post/123/456'), isNull); - }); - }); - - group('parseCommentId', () { - test('parses PieFed comment URL', () { - final result = parseCommentId( - 'https://piefed.social/post/1663157/comment/9679172'); - expect(result?.value, '9679172'); - expect(result?.instance, 'piefed.social'); - }); - - test('parses Lemmy legacy comment URL', () { - final result = parseCommentId('https://lemmy.world/comment/12345'); - expect(result?.value, '12345'); - expect(result?.instance, 'lemmy.world'); - }); - - test('parses Lemmy new format comment URL', () { - final result = parseCommentId('https://lemmy.world/post/123/456'); - expect(result?.value, '456'); - expect(result?.instance, 'lemmy.world'); - }); - }); - }); - - group('Real-world PieFed URL examples from issue #2036', () { - test('https://piefed.social/post/1656906', () { - final result = parsePostId('https://piefed.social/post/1656906'); - expect(result, isNotNull); - expect(result!.value, '1656906'); - expect(result.instance, 'piefed.social'); - }); - - test('https://piefed.social/u/darklightxi@lemmy.ca', () { - final result = parseUser('https://piefed.social/u/darklightxi@lemmy.ca'); - expect(result, isNotNull); - expect(result!.value, 'darklightxi'); - expect(result.instance, 'lemmy.ca'); - }); - - test('https://piefed.social/c/news@lemmy.world', () { - final result = parseCommunity('https://piefed.social/c/news@lemmy.world'); - expect(result, isNotNull); - expect(result!.value, 'news'); - expect(result.instance, 'lemmy.world'); - }); - - test('https://piefed.social/post/1663157/comment/9679172', () { - final result = - parseCommentId('https://piefed.social/post/1663157/comment/9679172'); - expect(result, isNotNull); - expect(result!.value, '9679172'); - expect(result.instance, 'piefed.social'); - }); - - test( - 'https://piefed.social/c/thunder_app/p/1422697/thunder-release-v0-8-0-initial-piefed-support (community post format)', - () { - final result = parsePostId( - 'https://piefed.social/c/thunder_app/p/1422697/thunder-release-v0-8-0-initial-piefed-support'); - expect(result, isNotNull); - expect(result!.value, '1422697'); - expect(result.instance, 'piefed.social'); - }); - }); - - group('PieFed Community Post URL Format', () { - test('parses /c/community/p/postId/slug format', () { - final result = parsePiefedPostId( - 'https://piefed.social/c/thunder_app/p/1422697/thunder-release-v0-8-0-initial-piefed-support'); - expect(result, isNotNull); - expect(result!.value, '1422697'); - expect(result.instance, 'piefed.social'); - }); - - test('parses /c/community/p/postId format (no slug)', () { - final result = - parsePiefedPostId('https://piefed.social/c/thunder_app/p/1422697'); - expect(result, isNotNull); - expect(result!.value, '1422697'); - expect(result.instance, 'piefed.social'); - }); - - test('unified parsePostId handles PieFed community post format', () { - final result = parsePostId( - 'https://piefed.social/c/thunder_app/p/1422697/thunder-release-v0-8-0-initial-piefed-support'); - expect(result, isNotNull); - expect(result!.value, '1422697'); - expect(result.instance, 'piefed.social'); - }); - }); -} diff --git a/test/utils/user_groups_test.dart b/test/utils/user_groups_test.dart deleted file mode 100644 index d49ccee5c..000000000 --- a/test/utils/user_groups_test.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'package:thunder/src/foundation/primitives/enums/user_type.dart'; -import 'package:thunder/src/foundation/contracts/preferences_store.dart'; -import 'package:thunder/src/foundation/persistence/preferences.dart'; -import 'package:thunder/src/features/settings/api.dart'; -import 'package:thunder/src/features/user/user.dart'; - -import '../widgets/base_widget.dart'; - -void main() { - setUpAll(() async { - TestWidgetsFlutterBinding.ensureInitialized(); - SharedPreferences.setMockInitialValues({}); - await UserPreferences.instance.initialize(); - }); - - group('Test user group logic', () { - testWidgets( - 'fetchUsernameDescriptor returns empty string if user is in no groups', - (tester) async { - await tester.pumpWidget(const BaseWidget()); - - String result = fetchUserGroupDescriptor([], null); - expect(result, ''); - }); - - testWidgets( - 'fetchUsernameDescriptor returns correct string for a single group', - (tester) async { - await tester.pumpWidget(const BaseWidget()); - - String result = fetchUserGroupDescriptor([UserType.admin], null); - expect(result, ' (Admin)'); - }); - - testWidgets( - 'fetchUsernameDescriptor returns correct string for multiple groups', - (tester) async { - await tester.pumpWidget(const BaseWidget()); - - String result = - fetchUserGroupDescriptor([UserType.admin, UserType.moderator], null); - expect(result, ' (Admin, Moderator)'); - }); - - testWidgets('fetchUsernameColor returns no color if user is in no groups', - (tester) async { - await tester.pumpWidget(BaseWidget( - child: BlocProvider( - create: (context) => ThemePreferencesCubit( - preferencesStore: const UserPreferencesStore()), - child: Builder(builder: (context) { - Color? color = fetchUserGroupColor(context, []); - - expect(color, isNull); - return Container(); - }), - ), - )); - }); - - testWidgets( - 'fetchUsernameColor returns correct color if user is in a single group', - (tester) async { - await tester.pumpWidget(BaseWidget( - child: BlocProvider( - create: (context) => ThemePreferencesCubit( - preferencesStore: const UserPreferencesStore()), - child: Builder(builder: (context) { - final theme = Theme.of(context); - - Color? color = fetchUserGroupColor(context, [UserType.moderator]); - Color? expectedColor = HSLColor.fromColor( - Color.alphaBlend( - theme.colorScheme.primaryContainer.withValues(alpha: 0.35), - UserType.moderator.color), - ).withLightness(0.85).toColor(); - - expect(color, expectedColor); - return Container(); - }), - ), - )); - }); - - testWidgets( - 'fetchUsernameColor returns correct color if user is in multiple groups', - (tester) async { - await tester.pumpWidget(BaseWidget( - child: BlocProvider( - create: (context) => ThemePreferencesCubit( - preferencesStore: const UserPreferencesStore()), - child: Builder(builder: (context) { - final theme = Theme.of(context); - - // The order of precedence is op -> self -> admin -> moderator -> bot - Color? color = fetchUserGroupColor( - context, [UserType.moderator, UserType.admin, UserType.self]); - Color? expectedColor = HSLColor.fromColor( - Color.alphaBlend( - theme.colorScheme.primaryContainer.withValues(alpha: 0.35), - UserType.self.color), - ).withLightness(0.85).toColor(); - - expect(color, expectedColor); - return Container(); - }), - ), - )); - }); - }); -} diff --git a/test/widgets/base_widget.dart b/test/widgets/base_widget.dart deleted file mode 100644 index bc893a938..000000000 --- a/test/widgets/base_widget.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:thunder/l10n/generated/app_localizations.dart'; - -import 'package:thunder/src/foundation/config/global_context.dart'; - -/// Base widget for simple tests which requires localization -class BaseWidget extends StatelessWidget { - const BaseWidget({super.key, this.child}); - - final Widget? child; - - @override - Widget build(BuildContext context) { - return MaterialApp( - locale: const Locale('en'), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - scaffoldMessengerKey: GlobalContext.scaffoldMessengerKey, - home: child, - ); - } -}