Skip to content

Commit

Permalink
feat: add send reply UI (#124)
Browse files Browse the repository at this point in the history
### What does this PR do?
This PR adds UI and additional logic for sending a post reply

### Changes Brought by This PR
- Added assets
- Added `ReplySentNotification` to display success message
- Added `ReplyDataNotifier` to hold reply data (only text for now)
- Added `SendReplyRequestNotifier` to simulate sending request
- Refactored PostDetailsPage and ReplyExpandedPage to use `postId`
instead of `postData`

![CleanShot 2024-07-29 at 09 42
35@2x](https://github.com/user-attachments/assets/25289576-3d39-4b65-9dad-86e078c24ccc)

---------

Co-authored-by: ice-tychon <https://github.com/ice-blockchain/flutter-app/commits/master/>
  • Loading branch information
ice-tychon authored Jul 30, 2024
1 parent 982eec7 commit f57a6c0
Show file tree
Hide file tree
Showing 20 changed files with 267 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.
24 changes: 24 additions & 0 deletions lib/app/extensions/async_value_listener.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';

extension AsyncValueListener on WidgetRef {
void listenAsyncValue<TSuccess>(
ProviderListenable<AsyncValue<TSuccess?>> provider, {
void Function()? onLoading,
void Function(TSuccess? response)? onSuccess,
void Function(Object erorr, StackTrace stackTrace)? onFailure,
bool skipLoadingOnReload = false,
bool skipLoadingOnRefresh = true,
bool skipError = false,
}) {
listen(provider, (previous, next) {
next.whenOrNull(
loading: onLoading,
data: onSuccess,
error: onFailure,
skipError: skipError,
skipLoadingOnRefresh: skipLoadingOnRefresh,
skipLoadingOnReload: skipLoadingOnReload,
);
});
}
}
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,15 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'send_reply_request_notifier.g.dart';

@riverpod
class SendReplyRequestNotifier extends _$SendReplyRequestNotifier {
@override
FutureOr<void> build() {}

Future<void> sendReply() async {
state = AsyncValue.loading();
await Future<void>.delayed(Duration(seconds: 1));
state = AsyncValue.data(null);
}
}
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 ?? () {},
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,40 @@
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/app/extensions/async_value_listener.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 = useState(false);
_listenReplySentNotification(ref, showReplySentNotification);

return Scaffold(
appBar: NavigationAppBar.screen(
title: Text(context.i18n.post_page_title),
Expand All @@ -38,6 +54,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 +88,18 @@ class PostDetailsPage extends StatelessWidget {
),
);
}

void _listenReplySentNotification(
WidgetRef ref,
ValueNotifier<bool> showReplySentNotification,
) {
ref.listenAsyncValue(
sendReplyRequestNotifierProvider,
onSuccess: (response) async {
showReplySentNotification.value = true;
await Future<void>.delayed(Duration(seconds: 2));
showReplySentNotification.value = false;
},
);
}
}
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 f57a6c0

Please sign in to comment.