merge: 合并 K40-B 评论登录闸
This commit is contained in:
commit
457d2fa6f2
9 files changed
+177
-41
No files matched your search
@@ -202,6 +202,7 @@
|
|||||||
"share": "Share",
|
"share": "Share",
|
||||||
"noComments": "No comments yet.",
|
"noComments": "No comments yet.",
|
||||||
"writeComment": "Write a comment",
|
"writeComment": "Write a comment",
|
||||||
|
"commentLoginHint": "Sign in to comment",
|
||||||
"postComment": "Post",
|
"postComment": "Post",
|
||||||
"loadMore": "Load more",
|
"loadMore": "Load more",
|
||||||
"commentsLoadFailed": "Comments could not be loaded.",
|
"commentsLoadFailed": "Comments could not be loaded.",
|
||||||
|
|||||||
@@ -176,6 +176,7 @@
|
|||||||
"share": "Bagikan",
|
"share": "Bagikan",
|
||||||
"noComments": "Belum ada komentar.",
|
"noComments": "Belum ada komentar.",
|
||||||
"writeComment": "Tulis komentar",
|
"writeComment": "Tulis komentar",
|
||||||
|
"commentLoginHint": "Masuk untuk berkomentar",
|
||||||
"postComment": "Kirim",
|
"postComment": "Kirim",
|
||||||
"loadMore": "Muat lebih banyak",
|
"loadMore": "Muat lebih banyak",
|
||||||
"commentsLoadFailed": "Komentar tidak dapat dimuat.",
|
"commentsLoadFailed": "Komentar tidak dapat dimuat.",
|
||||||
|
|||||||
@@ -922,6 +922,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Write a comment'**
|
/// **'Write a comment'**
|
||||||
String get writeComment;
|
String get writeComment;
|
||||||
|
|
||||||
|
/// No description provided for @commentLoginHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Sign in to comment'**
|
||||||
|
String get commentLoginHint;
|
||||||
|
|
||||||
/// No description provided for @postComment.
|
/// No description provided for @postComment.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -448,6 +448,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeComment => 'Write a comment';
|
String get writeComment => 'Write a comment';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commentLoginHint => 'Sign in to comment';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get postComment => 'Post';
|
String get postComment => 'Post';
|
||||||
|
|
||||||
|
|||||||
@@ -449,6 +449,9 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeComment => 'Tulis komentar';
|
String get writeComment => 'Tulis komentar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commentLoginHint => 'Masuk untuk berkomentar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get postComment => 'Kirim';
|
String get postComment => 'Kirim';
|
||||||
|
|
||||||
|
|||||||
@@ -443,6 +443,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get writeComment => '写评论';
|
String get writeComment => '写评论';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commentLoginHint => '登录后参与评论';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get postComment => '发布';
|
String get postComment => '发布';
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,7 @@
|
|||||||
"share": "分享",
|
"share": "分享",
|
||||||
"noComments": "还没有评论。",
|
"noComments": "还没有评论。",
|
||||||
"writeComment": "写评论",
|
"writeComment": "写评论",
|
||||||
|
"commentLoginHint": "登录后参与评论",
|
||||||
"postComment": "发布",
|
"postComment": "发布",
|
||||||
"loadMore": "加载更多",
|
"loadMore": "加载更多",
|
||||||
"commentsLoadFailed": "评论加载失败,请重试。",
|
"commentsLoadFailed": "评论加载失败,请重试。",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../../../core/data/models/drama.dart';
|
|||||||
import '../../../core/data/repositories/comment_repository.dart';
|
import '../../../core/data/repositories/comment_repository.dart';
|
||||||
import '../../../core/data/repositories/interaction_repository.dart';
|
import '../../../core/data/repositories/interaction_repository.dart';
|
||||||
import '../../../core/data/services/comment_controller.dart';
|
import '../../../core/data/services/comment_controller.dart';
|
||||||
|
import '../../../core/data/services/auth_controller.dart';
|
||||||
import '../../../core/formatting/error_text.dart';
|
import '../../../core/formatting/error_text.dart';
|
||||||
import '../../../core/platform/haptics.dart';
|
import '../../../core/platform/haptics.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
@@ -64,8 +65,8 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
|||||||
|
|
||||||
Future<void> _post() async {
|
Future<void> _post() async {
|
||||||
if (_controller.submitting) return;
|
if (_controller.submitting) return;
|
||||||
if (_input.text.trim().isEmpty) return;
|
|
||||||
if (!await ensureSignedIn(context) || !mounted) return;
|
if (!await ensureSignedIn(context) || !mounted) return;
|
||||||
|
if (_input.text.trim().isEmpty) return;
|
||||||
unawaited(context.read<Haptics>().lightImpact());
|
unawaited(context.read<Haptics>().lightImpact());
|
||||||
final result = await _controller.publish(_input.text, replyTo: _replyTo);
|
final result = await _controller.publish(_input.text, replyTo: _replyTo);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -90,6 +91,13 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
|||||||
setState(() => _replyTo = null);
|
setState(() => _replyTo = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _authenticateForComment() async {
|
||||||
|
_inputFocus.unfocus();
|
||||||
|
await ensureSignedIn(context);
|
||||||
|
if (!mounted || !context.read<AuthController>().isSignedIn) return;
|
||||||
|
_inputFocus.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _like(DramaComment comment) async {
|
Future<void> _like(DramaComment comment) async {
|
||||||
if (!await ensureSignedIn(context) || !mounted) return;
|
if (!await ensureSignedIn(context) || !mounted) return;
|
||||||
unawaited(context.read<Haptics>().lightImpact());
|
unawaited(context.read<Haptics>().lightImpact());
|
||||||
@@ -351,44 +359,52 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _composer(AppLocalizations l10n) => Row(
|
Widget _composer(AppLocalizations l10n) {
|
||||||
children: [
|
final signedIn = context.watch<AuthController>().isSignedIn;
|
||||||
Expanded(
|
return Row(
|
||||||
child: SizedBox(
|
children: [
|
||||||
height: _composerHeight,
|
Expanded(
|
||||||
child: TextField(
|
child: SizedBox(
|
||||||
key: const Key('comment-input'),
|
height: _composerHeight,
|
||||||
controller: _input,
|
child: TextField(
|
||||||
focusNode: _inputFocus,
|
key: const Key('comment-input'),
|
||||||
maxLength: _maxCommentLength,
|
controller: _input,
|
||||||
maxLines: 1,
|
focusNode: _inputFocus,
|
||||||
textAlignVertical: TextAlignVertical.center,
|
readOnly: !signedIn,
|
||||||
style: const TextStyle(color: AppColors.cream, fontSize: 14),
|
canRequestFocus: signedIn,
|
||||||
cursorColor: AppColors.blue,
|
maxLength: _maxCommentLength,
|
||||||
decoration: _composerDecoration(l10n.writeComment),
|
maxLines: 1,
|
||||||
onSubmitted: (_) => _post(),
|
textAlignVertical: TextAlignVertical.center,
|
||||||
|
style: const TextStyle(color: AppColors.cream, fontSize: 14),
|
||||||
|
cursorColor: AppColors.blue,
|
||||||
|
decoration: _composerDecoration(
|
||||||
|
signedIn ? l10n.writeComment : l10n.commentLoginHint,
|
||||||
|
),
|
||||||
|
onTap: signedIn ? null : _authenticateForComment,
|
||||||
|
onSubmitted: (_) => _post(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(width: 8),
|
SizedBox.square(
|
||||||
SizedBox.square(
|
dimension: _sendButtonSize,
|
||||||
dimension: _sendButtonSize,
|
child: IconButton.filled(
|
||||||
child: IconButton.filled(
|
key: const Key('comment-post'),
|
||||||
key: const Key('comment-post'),
|
tooltip: l10n.postComment,
|
||||||
tooltip: l10n.postComment,
|
padding: EdgeInsets.zero,
|
||||||
padding: EdgeInsets.zero,
|
iconSize: 18,
|
||||||
iconSize: 18,
|
onPressed: _controller.submitting ? null : _post,
|
||||||
onPressed: _controller.submitting ? null : _post,
|
icon: _controller.submitting
|
||||||
icon: _controller.submitting
|
? const SizedBox.square(
|
||||||
? const SizedBox.square(
|
dimension: 16,
|
||||||
dimension: 16,
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
)
|
||||||
)
|
: const Icon(Icons.send_rounded),
|
||||||
: const Icon(Icons.send_rounded),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CommentThread extends StatelessWidget {
|
class _CommentThread extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// Author: fruit.com.cn
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:fruitshorts/core/data/models/comment.dart';
|
import 'package:fruitshorts/core/data/models/comment.dart';
|
||||||
@@ -41,6 +43,71 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('should_gate_guest_comment_input_without_focus', (tester) async {
|
||||||
|
SharedPreferencesAsyncPlatform.instance =
|
||||||
|
InMemorySharedPreferencesAsync.empty();
|
||||||
|
final repository = FakeCommentRepository(pages: [const CommentPage()]);
|
||||||
|
|
||||||
|
await _pumpSheet(tester, repository: repository);
|
||||||
|
final input = find.byKey(const Key('comment-input'));
|
||||||
|
final field = tester.widget<TextField>(input);
|
||||||
|
|
||||||
|
expect(field.decoration?.hintText, 'Sign in to comment');
|
||||||
|
expect(field.focusNode?.hasFocus, isFalse);
|
||||||
|
await tester.tap(input);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Sign in to continue'), findsOneWidget);
|
||||||
|
expect(tester.widget<TextField>(input).focusNode?.hasFocus, isFalse);
|
||||||
|
await tester.tap(find.text('Not now'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.widget<TextField>(input).focusNode?.hasFocus, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should_focus_comment_input_after_successful_login', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
SharedPreferencesAsyncPlatform.instance =
|
||||||
|
InMemorySharedPreferencesAsync.empty();
|
||||||
|
final repository = FakeCommentRepository(pages: [const CommentPage()]);
|
||||||
|
final auth = await _pumpSheet(tester, repository: repository);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('comment-input')));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
await tester.tap(find.text('Sign In'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
(auth as _TestAuthController).setSignedIn(true);
|
||||||
|
await tester.pump();
|
||||||
|
Navigator.of(tester.element(find.byKey(const Key('auth-landing')))).pop();
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester
|
||||||
|
.widget<TextField>(find.byKey(const Key('comment-input')))
|
||||||
|
.focusNode
|
||||||
|
?.hasFocus,
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should_gate_guest_post_without_creating_comment', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
SharedPreferencesAsyncPlatform.instance =
|
||||||
|
InMemorySharedPreferencesAsync.empty();
|
||||||
|
final repository = FakeCommentRepository(pages: [const CommentPage()]);
|
||||||
|
|
||||||
|
await _pumpSheet(tester, repository: repository);
|
||||||
|
await tester.tap(find.byKey(const Key('comment-post')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Sign in to continue'), findsOneWidget);
|
||||||
|
expect(repository.publishCalls, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_show_server_comments_and_append_next_cursor_page', (
|
testWidgets('should_show_server_comments_and_append_next_cursor_page', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@@ -392,7 +459,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pumpSheet(
|
Future<AuthController> _pumpSheet(
|
||||||
WidgetTester tester, {
|
WidgetTester tester, {
|
||||||
required FakeCommentRepository repository,
|
required FakeCommentRepository repository,
|
||||||
FakeInteractionRepository? interactions,
|
FakeInteractionRepository? interactions,
|
||||||
@@ -421,16 +488,51 @@ Future<void> _pumpSheet(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AuthController> _auth() async {
|
Future<AuthController> _auth() async {
|
||||||
final client = ApiClient(
|
final client = ApiClient(
|
||||||
baseUrl: 'http://127.0.0.1:8081',
|
baseUrl: 'http://127.0.0.1:8081',
|
||||||
adapter: FakeHttpAdapter(
|
adapter: FakeHttpAdapter((options) async {
|
||||||
(_) async => FakeHttpResponse('{"code":0,"message":"ok","data":null}'),
|
if (options.uri.path == '/app/auth/login') {
|
||||||
),
|
return FakeHttpResponse(
|
||||||
|
jsonEncode({
|
||||||
|
'code': 0,
|
||||||
|
'message': 'ok',
|
||||||
|
'data': {
|
||||||
|
'accessToken': 'access',
|
||||||
|
'refreshToken': 'refresh',
|
||||||
|
'expiresIn': 7200,
|
||||||
|
'user': {'uid': '9', 'nickname': 'Viewer', 'avatarUrl': ''},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return FakeHttpResponse('{"code":0,"message":"ok","data":null}');
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
final store = AuthSessionStore();
|
||||||
|
final session = await store.read();
|
||||||
|
final auth = _TestAuthController(
|
||||||
|
AuthApi(client),
|
||||||
|
store,
|
||||||
|
signedIn: session != null,
|
||||||
);
|
);
|
||||||
final auth = AuthController(AuthApi(client), AuthSessionStore());
|
|
||||||
await auth.load();
|
await auth.load();
|
||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _TestAuthController extends AuthController {
|
||||||
|
_TestAuthController(super.api, super.store, {required this._signedIn});
|
||||||
|
|
||||||
|
bool _signedIn;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isSignedIn => _signedIn;
|
||||||
|
|
||||||
|
void setSignedIn(bool value) {
|
||||||
|
_signedIn = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user