[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: android comment input duplication

+172 -1
+26 -1
lib/src/features/posting/ui/widgets/mention_input_field.dart
··· 1 1 import 'package:flutter/material.dart'; 2 + import 'package:flutter/scheduler.dart'; 2 3 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 4 import 'package:spark/src/core/design_system/components/molecules/input_field.dart'; 4 5 import 'package:spark/src/core/design_system/tokens/constants.dart'; ··· 91 92 final hadSuggestions = _showSuggestions || _queryStartIndex != null; 92 93 93 94 if (clearTypeahead) { 94 - ref.read(actorTypeaheadProvider.notifier).clear(); 95 + _clearTypeahead(); 95 96 } 96 97 97 98 if (!hadSuggestions) { ··· 108 109 109 110 _showSuggestions = false; 110 111 _queryStartIndex = null; 112 + } 113 + 114 + void _clearTypeahead() { 115 + void clearTypeahead() { 116 + if (!mounted) { 117 + return; 118 + } 119 + 120 + ref.read(actorTypeaheadProvider.notifier).clear(); 121 + } 122 + 123 + final schedulerPhase = SchedulerBinding.instance.schedulerPhase; 124 + final isBuildPhase = 125 + schedulerPhase == SchedulerPhase.persistentCallbacks || 126 + schedulerPhase == SchedulerPhase.midFrameMicrotasks; 127 + 128 + if (isBuildPhase) { 129 + WidgetsBinding.instance.addPostFrameCallback((_) { 130 + clearTypeahead(); 131 + }); 132 + return; 133 + } 134 + 135 + clearTypeahead(); 111 136 } 112 137 113 138 void _syncMentions({
+146
test/features/posting/ui/widgets/mention_input_field_test.dart
··· 1 + import 'package:atproto/core.dart'; 2 + import 'package:flutter/material.dart'; 3 + import 'package:flutter_test/flutter_test.dart'; 4 + import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 + import 'package:get_it/get_it.dart'; 6 + import 'package:spark/src/core/network/atproto/data/models/actor_models.dart'; 7 + import 'package:spark/src/core/network/atproto/data/repositories/actor_repository.dart'; 8 + import 'package:spark/src/core/utils/logging/log_service.dart'; 9 + import 'package:spark/src/features/posting/models/mention.dart'; 10 + import 'package:spark/src/features/posting/models/mention_controller.dart'; 11 + import 'package:spark/src/features/posting/ui/widgets/mention_input_field.dart'; 12 + import 'package:spark/src/features/search/providers/actor_typeahead_provider.dart'; 13 + 14 + void main() { 15 + final getIt = GetIt.instance; 16 + 17 + setUp(() async { 18 + await getIt.reset(); 19 + getIt 20 + ..registerSingleton<LogService>(LogService()) 21 + ..registerSingleton<ActorRepository>(_FakeActorRepository()); 22 + }); 23 + 24 + tearDown(() async { 25 + await getIt.reset(); 26 + }); 27 + 28 + testWidgets( 29 + 'disabling the field while a mention query is active does not throw', 30 + (tester) async { 31 + final container = ProviderContainer(); 32 + addTearDown(container.dispose); 33 + 34 + await tester.pumpWidget( 35 + UncontrolledProviderScope( 36 + container: container, 37 + child: const MaterialApp( 38 + home: Scaffold(body: _MentionInputFieldHost()), 39 + ), 40 + ), 41 + ); 42 + 43 + await tester.enterText(find.byType(TextField), '@spark'); 44 + await tester.pump(); 45 + 46 + expect(container.read(actorTypeaheadProvider).query, 'spark'); 47 + 48 + await tester.tap(find.text('Disable')); 49 + await tester.pump(); 50 + 51 + expect(tester.takeException(), isNull); 52 + expect(find.byType(TextField), findsOneWidget); 53 + 54 + await tester.pump(); 55 + 56 + expect(container.read(actorTypeaheadProvider).query, isEmpty); 57 + }, 58 + ); 59 + } 60 + 61 + class _MentionInputFieldHost extends StatefulWidget { 62 + const _MentionInputFieldHost(); 63 + 64 + @override 65 + State<_MentionInputFieldHost> createState() => _MentionInputFieldHostState(); 66 + } 67 + 68 + class _MentionInputFieldHostState extends State<_MentionInputFieldHost> { 69 + final MentionController _controller = MentionController(); 70 + bool _enabled = true; 71 + 72 + @override 73 + void dispose() { 74 + _controller.dispose(); 75 + super.dispose(); 76 + } 77 + 78 + @override 79 + Widget build(BuildContext context) { 80 + return Column( 81 + mainAxisSize: MainAxisSize.min, 82 + children: [ 83 + MentionInputField( 84 + controller: _controller, 85 + onMentionsChanged: _onMentionsChanged, 86 + hintText: 'Comment', 87 + enabled: _enabled, 88 + ), 89 + TextButton( 90 + onPressed: () { 91 + setState(() { 92 + _enabled = false; 93 + }); 94 + }, 95 + child: const Text('Disable'), 96 + ), 97 + ], 98 + ); 99 + } 100 + 101 + void _onMentionsChanged(List<Mention> mentions) {} 102 + } 103 + 104 + class _FakeActorRepository implements ActorRepository { 105 + @override 106 + Future<ProfileViewDetailed> getProfile( 107 + String did, { 108 + bool useBluesky = false, 109 + }) { 110 + throw UnimplementedError(); 111 + } 112 + 113 + @override 114 + Future<List<ProfileViewDetailed>> getProfiles( 115 + List<String> dids, { 116 + bool useBluesky = false, 117 + }) { 118 + throw UnimplementedError(); 119 + } 120 + 121 + @override 122 + Future<bool> isEarlySupporter(String did) async => false; 123 + 124 + @override 125 + Future<SearchActorsResponse> searchActors( 126 + String query, { 127 + String? cursor, 128 + }) async { 129 + return SearchActorsResponse(actors: const []); 130 + } 131 + 132 + @override 133 + Future<SearchActorsTypeaheadResponse> searchActorsTypeahead( 134 + String query, { 135 + int limit = 10, 136 + }) async { 137 + return SearchActorsTypeaheadResponse(actors: const []); 138 + } 139 + 140 + @override 141 + Future<void> updateProfile({ 142 + required String displayName, 143 + required String description, 144 + Blob? avatar, 145 + }) async {} 146 + }