Skip to content

Commit

Permalink
feat: add send reply UI
Browse files Browse the repository at this point in the history
  • Loading branch information
ice-tychon committed Jul 29, 2024
1 parent 029eba1 commit b60dd55
Show file tree
Hide file tree
Showing 19 changed files with 253 additions and 31 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/icons/icon_block_checkbox_on_white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions lib/app/features/feed/model/post_reply/post_reply_data.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';

part 'post_reply_data.freezed.dart';

@Freezed(copyWith: true)
class PostReplyData with _$PostReplyData {
const factory PostReplyData({
required String text,
}) = _PostReplyData;

factory PostReplyData.empty() => PostReplyData(
text: '',
);
}
20 changes: 20 additions & 0 deletions lib/app/features/feed/providers/post_by_id_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'dart:convert';

import 'package:ice/app/features/feed/model/post/post_data.dart';
import 'package:nostr_dart/nostr_dart.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'post_by_id_provider.g.dart';

@riverpod
PostData? postById(PostByIdRef ref, {
required String id,
}) {
return PostData.fromEventMessage(
EventMessage.fromJson(
json.decode(
r'["EVENT","5f6556d1-9a5e-4092-a7e3-a202857b445f",{"content":"GM https://image.nostr.build/d84c3d3a7abfa358106cad5a3ec0cc0888733f4cacda2b49cf3d7f9519003698.jpg","created_at":1720428050,"id":"0454657a5edeedf3db10b37dd5a3ca387f5714a2675f7e51539475e8fcb331de","kind":1,"pubkey":"d0c01dd5931409d2bc7e58ee4908e6366ff0fd722d20e9c709fde6846f3ceabb","sig":"263b97d9157f602b944859ebd3fe56851a5a786379bf38f7e81f6a58ec7acc5bca412f1e3a488ada033e240977bbc134f2f5047e4a9df32dcd19d96126c8a9ed","tags":[["e","6f2f2e100c8075d5ebae5866544ae243aad9e56916c3cb33e7a69c69004858e6","","root"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["r","https://image.nostr.build/d84c3d3a7abfa358106cad5a3ec0cc0888733f4cacda2b49cf3d7f9519003698.jpg"],["imeta","url https://image.nostr.build/d84c3d3a7abfa358106cad5a3ec0cc0888733f4cacda2b49cf3d7f9519003698.jpg","m image/jpeg","alt Verifiable file url","x 1dc63792be80c939b207f089f69221df8755c0f6e38503f666e4619f1ccf9a12","size 340088","dim 864x1920","blurhash [[email protected];W?nzbdEFo$WBj]NPahjqj]o}bIofWUt7oHWEj=IVocjYa}Mwn$WYfRsiW=a#oI","ox d84c3d3a7abfa358106cad5a3ec0cc0888733f4cacda2b49cf3d7f9519003698"]]}]',
) as List<dynamic>,
),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:ice/app/features/feed/model/post_reply/post_reply_data.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'reply_data_notifier.g.dart';

@riverpod
class ReplyDataNotifier extends _$ReplyDataNotifier {
@override
PostReplyData build() {
return PostReplyData.empty();
}

void onTextChanged(String newValue) {
state = state.copyWith(text: newValue);
}

void clear() {
state = PostReplyData.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:ice/app/features/feed/providers/post_reply/reply_data_notifier.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'send_reply_request_notifier.g.dart';

@riverpod
class SendReplyRequestNotifier extends _$SendReplyRequestNotifier {
@override
Future<bool?> build() async {
return null;
}

Future<void> sendReply() async {
state = AsyncValue.loading();

// send reply
await Future<void>.delayed(Duration(seconds: 1));
ref.read(replyDataNotifierProvider.notifier).clear();

state = AsyncValue.data(true);
}
}
2 changes: 1 addition & 1 deletion lib/app/features/feed/views/components/post/post.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class Post extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => PostDetailsRoute($extra: postData).push<void>(context),
onTap: () => PostDetailsRoute($extra: postData.id).push<void>(context),
child: ScreenSideOffset.small(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ class PostRepliesActionBar extends StatelessWidget {
const PostRepliesActionBar({
this.padding,
this.shadowBuilder,
this.onSendPressed,
super.key,
});

final VoidCallback? onSendPressed;

factory PostRepliesActionBar.withShadow({EdgeInsets? padding}) => PostRepliesActionBar(
padding: padding ?? EdgeInsets.symmetric(horizontal: 16.0.s),
shadowBuilder: _defaultShadowBuilder,
Expand Down Expand Up @@ -57,7 +60,7 @@ class PostRepliesActionBar extends StatelessWidget {
Button(
minimumSize: Size(48.0.s, 28.0.s),
borderRadius: BorderRadius.circular(100.0.s),
onPressed: () {},
onPressed: onSendPressed == null ? () {} : onSendPressed!,
leadingIcon: Padding(
padding: EdgeInsets.symmetric(vertical: 4.0.s),
child: Assets.images.icons.iconFeedSendbutton.icon(size: 20.0.s),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:ice/app/extensions/extensions.dart';
import 'package:ice/app/router/components/navigation_app_bar/navigation_app_bar.dart';

class PostNotFound extends StatelessWidget {
const PostNotFound({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: NavigationAppBar.screen(
title: Text(context.i18n.post_page_title),
),
body: Center(
child: Text('Post not found'),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:ice/app/extensions/asset_gen_image.dart';
import 'package:ice/app/extensions/build_context.dart';
import 'package:ice/app/extensions/num.dart';
import 'package:ice/app/extensions/theme_data.dart';
import 'package:ice/app/features/feed/model/post/post_data.dart';
import 'package:ice/app/features/feed/providers/post_reply/reply_data_notifier.dart';
import 'package:ice/app/features/feed/providers/post_reply/send_reply_request_notifier.dart';
import 'package:ice/app/features/feed/views/pages/post_details_page/components/reply_input_field/components/reply_author_header.dart';
import 'package:ice/app/features/feed/views/components/post_replies/post_replies_action_bar.dart';
import 'package:ice/app/router/app_routes.dart';
import 'package:ice/generated/assets.gen.dart';

class ReplyInputField extends HookWidget {
class ReplyInputField extends HookConsumerWidget {
const ReplyInputField({
required this.postData,
super.key,
Expand All @@ -19,11 +22,14 @@ class ReplyInputField extends HookWidget {
final PostData postData;

@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final colors = context.theme.appColors;
final textThemes = context.theme.appTextThemes;

final isFocused = useState(false);
final textController = useTextEditingController(
text: ref.watch(replyDataNotifierProvider.select((data) => data.text)),
);

return Padding(
padding: EdgeInsets.symmetric(
Expand Down Expand Up @@ -52,6 +58,9 @@ class ReplyInputField extends HookWidget {
child: Focus(
onFocusChange: (value) => isFocused.value = value,
child: TextField(
controller: textController,
onChanged: (value) =>
ref.read(replyDataNotifierProvider.notifier).onTextChanged(value),
style: textThemes.body2,
decoration: InputDecoration(
hintText: context.i18n.post_reply_hint,
Expand All @@ -66,7 +75,10 @@ class ReplyInputField extends HookWidget {
),
if (isFocused.value)
GestureDetector(
onTap: () => ReplyExpandedRoute($extra: postData).push<void>(context),
onTap: () async {
await ReplyExpandedRoute($extra: postData.id).push<void>(context);
textController.text = ref.read(replyDataNotifierProvider).text;
},
child: Assets.images.icons.iconReplysearchScale.icon(size: 20.0.s),
),
],
Expand All @@ -75,7 +87,10 @@ class ReplyInputField extends HookWidget {
),
),
SizedBox(height: 12.0.s),
if (isFocused.value) const PostRepliesActionBar(),
if (isFocused.value)
PostRepliesActionBar(
onSendPressed: () => ref.read(sendReplyRequestNotifierProvider.notifier).sendReply(),
),
],
),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:ice/app/extensions/extensions.dart';
import 'package:ice/generated/assets.gen.dart';

class ReplySentNotification extends StatelessWidget {
const ReplySentNotification({super.key});

@override
Widget build(BuildContext context) {
final colors = context.theme.appColors;
final textStyles = context.theme.appTextThemes;

return SizedBox(
height: 54.0.s,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0.s),
color: colors.primaryAccent,
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 15.0.s),
child: Row(
children: [
Assets.images.icons.iconBlockCheckboxOnWhite.icon(),
SizedBox(width: 8.0.s),
Text(
context.i18n.post_reply_sent,
style: textStyles.subtitle2.copyWith(color: colors.onPrimaryAccent),
),
],
),
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:ice/app/components/screen_offset/screen_side_offset.dart';
import 'package:ice/app/extensions/extensions.dart';
import 'package:ice/app/features/feed/model/post/post_data.dart';
import 'package:ice/app/features/feed/providers/post_by_id_provider.dart';
import 'package:ice/app/features/feed/providers/post_reply/send_reply_request_notifier.dart';
import 'package:ice/app/features/feed/views/components/list_separator/list_separator.dart';
import 'package:ice/app/features/feed/views/components/post/components/post_footer/post_details_footer.dart';
import 'package:ice/app/features/feed/views/components/post/post.dart';
import 'package:ice/app/features/feed/views/components/post_list/components/post_list.dart';
import 'package:ice/app/features/feed/views/pages/post_details_page/components/post_not_found/post_not_found.dart';
import 'package:ice/app/features/feed/views/pages/post_details_page/components/reply_input_field/reply_input_field.dart';
import 'package:ice/app/features/feed/views/pages/post_details_page/components/reply_sent_notification/reply_sent_notification.dart';
import 'package:ice/app/router/components/navigation_app_bar/navigation_app_bar.dart';
import 'package:ice/generated/assets.gen.dart';

class PostDetailsPage extends StatelessWidget {
class PostDetailsPage extends HookConsumerWidget {
const PostDetailsPage({
required this.postData,
required this.postId,
super.key,
});

final PostData postData;
final String postId;

@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final postData = ref.watch(postByIdProvider(id: postId));

if (postData == null) {
return PostNotFound();
}

final showReplySentNotification = _listenReplySentNotification(ref);

return Scaffold(
appBar: NavigationAppBar.screen(
title: Text(context.i18n.post_page_title),
Expand All @@ -38,6 +52,18 @@ class PostDetailsPage extends StatelessWidget {
Flexible(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: AnimatedCrossFade(
crossFadeState: showReplySentNotification.value
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: Duration(milliseconds: 300),
firstChild: SizedBox.shrink(),
secondChild: ScreenSideOffset.small(
child: ReplySentNotification(),
),
),
),
SliverToBoxAdapter(
child: Post(
postData: postData,
Expand All @@ -60,4 +86,23 @@ class PostDetailsPage extends StatelessWidget {
),
);
}

ValueNotifier<bool> _listenReplySentNotification(WidgetRef ref) {
final showReplySentNotification = useState(false);

ref.listen(
sendReplyRequestNotifierProvider,
(previous, next) async {
if (previous == null || !previous.isLoading || next.hasError || next.valueOrNull != true) {
return;
}

showReplySentNotification.value = true;
await Future<void>.delayed(Duration(seconds: 2));
showReplySentNotification.value = false;
},
);

return showReplySentNotification;
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:ice/app/components/avatar/avatar.dart';
import 'package:ice/app/extensions/extensions.dart';
import 'package:ice/app/features/feed/providers/post_reply/reply_data_notifier.dart';
import 'package:ice/app/hooks/use_on_init.dart';

class ExpandedReplyInputField extends HookWidget {
class ExpandedReplyInputField extends HookConsumerWidget {
const ExpandedReplyInputField({super.key});

@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final colors = context.theme.appColors;
final textStyles = context.theme.appTextThemes;

final focusNode = useFocusNode();
useOnInit<void>(focusNode.requestFocus);

final textController = useTextEditingController(
text: ref.watch(replyDataNotifierProvider.select((data) => data.text)),
);

return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expand All @@ -27,6 +33,9 @@ class ExpandedReplyInputField extends HookWidget {
child: Padding(
padding: EdgeInsets.only(top: 4.0.s),
child: TextField(
controller: textController,
onChanged: (value) =>
ref.read(replyDataNotifierProvider.notifier).onTextChanged(value),
focusNode: focusNode,
maxLines: null,
minLines: 4,
Expand Down
Loading

0 comments on commit b60dd55

Please sign in to comment.