Skip to content

Commit

Permalink
feat: add hint feature flag (#360)
Browse files Browse the repository at this point in the history
* feat: fetch is hints enabled flag

* feat: update hint state with enabled flag

* feat: hide hint feature when disabled
  • Loading branch information
jsgalarraga authored Apr 23, 2024
1 parent d71888a commit 6457a0d
Show file tree
Hide file tree
Showing 15 changed files with 221 additions and 46 deletions.
17 changes: 17 additions & 0 deletions lib/hint/bloc/hint_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:api_client/api_client.dart';
import 'package:bloc/bloc.dart';
import 'package:board_info_repository/board_info_repository.dart';
import 'package:equatable/equatable.dart';
import 'package:game_domain/game_domain.dart';

Expand All @@ -9,15 +10,31 @@ part 'hint_state.dart';
class HintBloc extends Bloc<HintEvent, HintState> {
HintBloc({
required HintResource hintResource,
required BoardInfoRepository boardInfoRepository,
}) : _hintResource = hintResource,
_boardInfoRepository = boardInfoRepository,
super(const HintState()) {
on<HintEnabledRequested>(_onHintEnabledRequested);
on<HintModeEntered>(_onHintModeEntered);
on<HintModeExited>(_onHintModeExited);
on<HintRequested>(_onHintRequested);
on<PreviousHintsRequested>(_onPreviousHintsRequested);
}

final HintResource _hintResource;
final BoardInfoRepository _boardInfoRepository;

Future<void> _onHintEnabledRequested(
HintEnabledRequested event,
Emitter<HintState> emit,
) async {
await emit.forEach(
_boardInfoRepository.isHintsEnabled(),
onData: (isHintsEnabled) {
return state.copyWith(isHintsEnabled: isHintsEnabled);
},
);
}

void _onHintModeEntered(
HintModeEntered event,
Expand Down
7 changes: 7 additions & 0 deletions lib/hint/bloc/hint_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ sealed class HintEvent extends Equatable {
const HintEvent();
}

class HintEnabledRequested extends HintEvent {
const HintEnabledRequested();

@override
List<Object> get props => [];
}

class HintModeEntered extends HintEvent {
const HintModeEntered();

Expand Down
6 changes: 5 additions & 1 deletion lib/hint/bloc/hint_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,25 @@ enum HintStatus {

class HintState extends Equatable {
const HintState({
this.isHintsEnabled = false,
this.status = HintStatus.initial,
this.hints = const [],
this.maxHints = 10,
});

final bool isHintsEnabled;
final HintStatus status;
final List<Hint> hints;
final int maxHints;

HintState copyWith({
bool? isHintsEnabled,
HintStatus? status,
List<Hint>? hints,
int? maxHints,
}) {
return HintState(
isHintsEnabled: isHintsEnabled ?? this.isHintsEnabled,
status: status ?? this.status,
hints: hints ?? this.hints,
maxHints: maxHints ?? this.maxHints,
Expand All @@ -50,5 +54,5 @@ class HintState extends Equatable {
int get hintsLeft => maxHints - hints.length;

@override
List<Object> get props => [status, hints, maxHints];
List<Object> get props => [isHintsEnabled, status, hints, maxHints];
}
37 changes: 22 additions & 15 deletions lib/hint/widgets/hints_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,30 @@ class HintsSection extends StatelessWidget {
final isThinking = context
.select((HintBloc bloc) => bloc.state.status == HintStatus.thinking);
final allHints = context.select((HintBloc bloc) => bloc.state.hints);

return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
...allHints.mapIndexed(
(i, hint) => HintQuestionResponse(
index: i,
hint: hint,
final isHintsEnabled =
context.select((HintBloc bloc) => bloc.state.isHintsEnabled);

if (!isHintsEnabled) return const SizedBox.shrink();

return SingleChildScrollView(
reverse: true,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
...allHints.mapIndexed(
(i, hint) => HintQuestionResponse(
index: i,
hint: hint,
),
),
),
if (isThinking) ...[
const SizedBox(height: 24),
const Center(child: HintLoadingIndicator()),
const SizedBox(height: 8),
if (isThinking) ...[
const SizedBox(height: 24),
const Center(child: HintLoadingIndicator()),
const SizedBox(height: 8),
],
],
],
),
);
}
}
Expand Down
6 changes: 5 additions & 1 deletion lib/word_selection/view/word_selection_page.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:api_client/api_client.dart';
import 'package:board_info_repository/board_info_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_crossword/hint/hint.dart';
Expand All @@ -20,7 +21,10 @@ class WordSelectionPage extends StatelessWidget {
key: Key(wordId),
create: (context) => HintBloc(
hintResource: context.read<HintResource>(),
)..add(PreviousHintsRequested(wordId)),
boardInfoRepository: context.read<BoardInfoRepository>(),
)
..add(const HintEnabledRequested())
..add(PreviousHintsRequested(wordId)),
child: const WordSelectionView(),
);
}
Expand Down
40 changes: 23 additions & 17 deletions lib/word_selection/view/word_solving_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class WordSolvingLargeView extends StatelessWidget {
final selectedWord =
context.select((WordSelectionBloc bloc) => bloc.state.word);
if (selectedWord == null) return const SizedBox.shrink();
final isHintsEnabled =
context.select((HintBloc bloc) => bloc.state.isHintsEnabled);

return Column(
children: [
Expand All @@ -45,8 +47,10 @@ class WordSolvingLargeView extends StatelessWidget {
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
const HintsTitle(),
const SizedBox(height: 32),
if (isHintsEnabled) ...[
const HintsTitle(),
const SizedBox(height: 32),
],
Flexible(
child: BlocSelector<WordSelectionBloc, WordSelectionState,
WordSelectionStatus>(
Expand All @@ -56,10 +60,7 @@ class WordSolvingLargeView extends StatelessWidget {
return const CircularProgressIndicator();
}

return const SingleChildScrollView(
reverse: true,
child: HintsSection(),
);
return const HintsSection();
},
),
),
Expand Down Expand Up @@ -96,6 +97,8 @@ class _WordSolvingSmallViewState extends State<WordSolvingSmallView> {
final selectedWord =
context.select((WordSelectionBloc bloc) => bloc.state.word);
if (selectedWord == null) return const SizedBox.shrink();
final isHintsEnabled =
context.select((HintBloc bloc) => bloc.state.isHintsEnabled);

return Column(
children: [
Expand All @@ -112,8 +115,10 @@ class _WordSolvingSmallViewState extends State<WordSolvingSmallView> {
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
const HintsTitle(),
const SizedBox(height: 32),
if (isHintsEnabled) ...[
const HintsTitle(),
const SizedBox(height: 32),
],
Expanded(
child: BlocSelector<WordSelectionBloc, WordSelectionState,
WordSelectionStatus>(
Expand All @@ -123,10 +128,7 @@ class _WordSolvingSmallViewState extends State<WordSolvingSmallView> {
return const Center(child: CircularProgressIndicator());
}

return const SingleChildScrollView(
reverse: true,
child: HintsSection(),
);
return const HintsSection();
},
),
),
Expand All @@ -148,8 +150,10 @@ class BottomPanel extends StatelessWidget {
Widget build(BuildContext context) {
final isHintModeActive =
context.select((HintBloc bloc) => bloc.state.isHintModeActive);
final isHintsEnabled =
context.select((HintBloc bloc) => bloc.state.isHintsEnabled);

if (isHintModeActive) {
if (isHintModeActive && isHintsEnabled) {
return const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expand All @@ -166,10 +170,12 @@ class BottomPanel extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Flexible(
child: GeminiHintButton(),
),
const SizedBox(width: 8),
if (isHintsEnabled) ...[
const Flexible(
child: GeminiHintButton(),
),
const SizedBox(width: 8),
],
Flexible(
child: SubmitButton(controller: controller),
),
Expand Down
26 changes: 26 additions & 0 deletions packages/board_info_repository/lib/src/board_info_repository.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:rxdart/rxdart.dart';

/// {@template board_info_exception}
/// An exception to throw when there is an error fetching the board info.
Expand Down Expand Up @@ -34,6 +35,8 @@ class BoardInfoRepository {
/// The [CollectionReference] for the config.
late final CollectionReference<Map<String, dynamic>> boardInfoCollection;

BehaviorSubject<bool>? _hintsEnabled;

/// Returns the total words count available in the crossword.
Stream<int> getTotalWordsCount() {
try {
Expand Down Expand Up @@ -85,4 +88,27 @@ class BoardInfoRepository {
throw BoardInfoException(error, stackStrace);
}
}

/// Returns the hints enabled status.
Stream<bool> isHintsEnabled() {
if (_hintsEnabled != null) return _hintsEnabled!.stream;

_hintsEnabled = BehaviorSubject<bool>();

boardInfoCollection
.where('type', isEqualTo: 'is_hints_enabled')
.snapshots()
.map((snapshot) {
final docs = snapshot.docs;
// If the flag is not found, we assume it is disabled.
if (docs.isEmpty) return false;

final isHintsEnabled = docs.first.data()['value'] as bool;
return isHintsEnabled;
})
.listen(_hintsEnabled!.add)
.onError(_hintsEnabled!.addError);

return _hintsEnabled!.stream;
}
}
1 change: 1 addition & 0 deletions packages/board_info_repository/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies:
cloud_firestore: ^4.16.1
flutter:
sdk: flutter
rxdart: ^0.27.7

dev_dependencies:
flutter_test:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,22 @@ void main() {
);
});
});

group('isHintsEnabled', () {
test('returns hints enabled status from firebase', () {
final doc = _MockQueryDocumentSnapshot<Map<String, dynamic>>();
final query = _MockQuerySnapshot<Map<String, dynamic>>();
when(
() => collection.where('type', isEqualTo: 'is_hints_enabled'),
).thenReturn(collection);
when(collection.snapshots).thenAnswer((_) => Stream.value(query));
when(() => query.docs).thenReturn([doc]);
when(doc.data).thenReturn({'value': true});

final result = boardInfoRepository.isHintsEnabled();
expect(result, emits(true));
});
});
});

group('BoardInfoException', () {
Expand Down
Loading

0 comments on commit 6457a0d

Please sign in to comment.