From fc47e2dfb29056c0e713efd5eadd27721bada437 Mon Sep 17 00:00:00 2001 From: lucien144 Date: Fri, 9 Dec 2022 20:21:58 +0100 Subject: [PATCH] feat: new post context menu #322 --- ios/Podfile.lock | 10 +- ios/Runner.xcodeproj/project.pbxproj | 4 +- .../actionSheets/PostActionSheet.dart | 427 ++++++++++++++---- lib/components/mail_list_item.dart | 9 +- lib/components/post/post_list_item.dart | 222 +++------ lib/components/post/post_rating.dart | 206 +++++---- lib/components/post/post_thumbs.dart | 67 +-- lib/components/text_icon.dart | 6 +- lib/controllers/ApiController.dart | 22 +- lib/controllers/ApiProvider.dart | 15 +- lib/controllers/IApiProvider.dart | 4 +- lib/model/Mail.dart | 13 +- lib/model/Post.dart | 16 +- .../LoadRatingsNotification.dart | 3 - lib/model/post/ipost.dart | 11 + lib/model/reponses/DiscussionResponse.dart | 5 + lib/pages/DiscussionPage.dart | 11 +- lib/pages/NewMessagePage.dart | 4 +- lib/theme/L.dart | 2 +- pubspec.lock | 42 +- pubspec.yaml | 3 +- test/api_test.dart | 14 +- 22 files changed, 678 insertions(+), 438 deletions(-) delete mode 100644 lib/model/notifications/LoadRatingsNotification.dart create mode 100644 lib/model/post/ipost.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b0cba36..13cd9de 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -138,7 +138,7 @@ PODS: - Flutter - FlutterMacOS - Sentry (~> 7.23.0) - - share (0.0.1): + - share_plus (0.0.1): - Flutter - shared_preferences_ios (0.0.1): - Flutter @@ -169,7 +169,7 @@ DEPENDENCIES: - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - - share (from `.symlinks/plugins/share/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -221,8 +221,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" sentry_flutter: :path: ".symlinks/plugins/sentry_flutter/ios" - share: - :path: ".symlinks/plugins/share/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" shared_preferences_ios: :path: ".symlinks/plugins/shared_preferences_ios/ios" sqflite: @@ -263,7 +263,7 @@ SPEC CHECKSUMS: PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb Sentry: a0d4563fa4ddacba31fdcc35daaa8573d87224d6 sentry_flutter: 8bde7d0e57a721727fe573f13bb292c497b5a249 - share: 0b2c3e82132f5888bccca3351c504d0003b3b410 + share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e05c911..9e485ad 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -274,7 +274,7 @@ "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", "${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework", "${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework", - "${BUILT_PRODUCTS_DIR}/share/share.framework", + "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework", "${BUILT_PRODUCTS_DIR}/shared_preferences_ios/shared_preferences_ios.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", @@ -303,7 +303,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", diff --git a/lib/components/actionSheets/PostActionSheet.dart b/lib/components/actionSheets/PostActionSheet.dart index 299be47..ad1e518 100644 --- a/lib/components/actionSheets/PostActionSheet.dart +++ b/lib/components/actionSheets/PostActionSheet.dart @@ -1,15 +1,32 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:fyx/components/text_icon.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fyx/components/feedback_indicator.dart'; +import 'package:fyx/components/post/post_list_item.dart'; +import 'package:fyx/components/post/post_thumbs.dart'; import 'package:fyx/controllers/AnalyticsProvider.dart'; import 'package:fyx/controllers/ApiController.dart'; +import 'package:fyx/controllers/IApiProvider.dart'; +import 'package:fyx/model/Mail.dart'; +import 'package:fyx/model/MainRepository.dart'; +import 'package:fyx/model/Post.dart'; import 'package:fyx/model/post/Content.dart'; +import 'package:fyx/model/post/PostThumbItem.dart'; +import 'package:fyx/model/post/ipost.dart'; +import 'package:fyx/model/reponses/OkResponse.dart'; +import 'package:fyx/model/reponses/PostRatingsResponse.dart'; +import 'package:fyx/pages/DiscussionPage.dart'; +import 'package:fyx/pages/NewMessagePage.dart'; +import 'package:fyx/state/batch_actions_provider.dart'; import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; import 'package:fyx/theme/skin/Skin.dart'; import 'package:fyx/theme/skin/SkinColors.dart'; -import 'package:share/share.dart'; +import 'package:intl/intl.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:share_plus/share_plus.dart'; class ShareData { final String subject; @@ -19,106 +36,338 @@ class ShareData { ShareData({required this.subject, required this.body, required this.link}); } -class PostActionSheet extends StatefulWidget { +class PostContextMenu extends ConsumerStatefulWidget { + final T item; + final bool adminTools; final BuildContext parentContext; - final String user; - final int postId; final Function flagPostCallback; - final ShareData shareData; - PostActionSheet( - {Key? key, required this.user, required this.postId, required this.flagPostCallback, required this.parentContext, required this.shareData}) + PostContextMenu({Key? key, required this.item, required this.flagPostCallback, required this.parentContext, this.adminTools = false}) : super(key: key); @override - _PostActionSheetState createState() => _PostActionSheetState(); + _PostContextMenuState createState() => _PostContextMenuState(); } -class _PostActionSheetState extends State { +class _PostContextMenuState extends ConsumerState> { bool _reportIndicator = false; - int _deleteCounter = 0; + bool _reminderIndicator = false; + bool _deleteIndicator = false; + bool _bananaIndicator = false; + SkinColors? colors; + + bool get isMail => widget.item is Mail; + + bool get isPost => widget.item is Post; + + Mail get mail => widget.item as Mail; + + Post get post => widget.item as Post; + + Widget createGridView({required List children}) { + return GridView.count( + physics: NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(left: 16, top: 16, right: 16, bottom: 48), + crossAxisCount: 3, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + shrinkWrap: true, + children: children); + } + + void confirmationDialog(String title, String content, Function()? onPressed) { + showCupertinoDialog( + context: context, + builder: (BuildContext context) => new CupertinoAlertDialog( + title: Text(title), + content: Text(content), + actions: [ + CupertinoDialogAction( + child: Text('Ne'), + onPressed: () => Navigator.of(context, rootNavigator: true).pop(), + ), + CupertinoDialogAction( + child: Text('Ano'), + isDestructiveAction: true, + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + if (onPressed != null) onPressed(); + }, + ), + ], + )); + } + + Widget gridItem(String label, IconData? icon, {Function()? onTap, bool danger = false}) { + return GestureDetector( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration(color: danger ? colors?.danger.withOpacity(0.1) : colors?.barBackground, borderRadius: BorderRadius.circular(8)), + child: Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ + Icon(icon, size: 32, color: danger ? colors?.danger : colors?.primary), + Text( + label, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: danger ? colors?.danger : colors?.primary), + ) + ]), + ), + onTap: onTap); + } @override Widget build(BuildContext context) { - SkinColors colors = Skin.of(context).theme.colors; - - return CupertinoActionSheet( - actions: [ - Visibility( - visible: widget.shareData is ShareData, - child: CupertinoActionSheetAction( - child: TextIcon(L.POST_SHEET_COPY_LINK, icon: Icons.link), - onPressed: () { - var data = ClipboardData(text: widget.shareData.link); - Clipboard.setData(data).then((_) { - T.success(L.TOAST_COPIED, bg: colors.success); - Navigator.pop(context); + colors = Skin.of(context).theme.colors; + + return createGridView( + children: [ + gridItem('Zkopírovat text', MdiIcons.contentCopy, onTap: () { + var data = ClipboardData(text: widget.item.content.strippedContent); + Clipboard.setData(data).then((_) { + T.success(L.TOAST_COPIED, bg: colors!.success); + Navigator.pop(context); + }); + AnalyticsProvider().logEvent('copyPostBody'); + }), + if (isPost) + gridItem('Odpovědět soukromě', MdiIcons.reply, onTap: () { + Navigator.pop(context); // Close the sheet first. + Navigator.of(context, rootNavigator: true).pushNamed('/new-message', + arguments: NewMessageSettings( + hasInputField: true, + inputFieldPlaceholder: post.nick, + messageFieldPlaceholder: post.link, + onClose: () => T.success('👍 Zpráva poslána.', bg: colors!.success), + onSubmit: (String? inputField, String message, List> attachments) async { + if (inputField == null) return false; + + var response = await ApiController().sendMail(inputField, message, attachments: attachments); + return response.isOk; + })); + }), + if (isPost && post.canBeReminded) + FeedbackIndicator( + isLoading: _reminderIndicator, + child: gridItem( + post.hasReminder ? 'Odebrat z poznámek' : 'Uložit do poznámek', post.hasReminder ? MdiIcons.bookmark : MdiIcons.bookmarkOutline, + onTap: () { + setState(() => _reminderIndicator = true); + ApiController() + .setPostReminder(post.idKlub, post.id, !post.hasReminder) + .catchError((error) => T.error(L.REMINDER_ERROR, bg: colors!.danger)) + .then((response) { + post.hasReminder = !post.hasReminder; + }).whenComplete(() => setState(() => _reminderIndicator = false)); + AnalyticsProvider().logEvent(post.hasReminder ? 'reminder_remove' : 'reminder_add'); + })), + if (isPost && post.replies.length > 0) + gridItem( + 'Zobrazit\n${post.replies.length} ${Intl.plural(post.replies.length, one: 'odpověď', few: 'odpovědi', other: 'odpovědí', locale: 'cs_CZ')}', + MdiIcons.timelineText, onTap: () { + ApiController().loadDiscussion(post.idKlub, lastId: post.id, filterReplies: true).then((response) { + final postItems = response.posts.map((item) => PostListItem(Post.fromJson(item, post.idKlub))).toList(); + showCupertinoModalBottomSheet( + context: context, + builder: (BuildContext context) { + return SingleChildScrollView( + controller: ModalScrollController.of(context), + child: Container( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + children: [ + Container(child: PostListItem(post), color: colors!.primary.withOpacity(.1)), + ListView.builder( + itemBuilder: (context, index) { + return postItems[index]; + }, + itemCount: postItems.length, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true), + ], + ), + ), + ); }); - AnalyticsProvider().logEvent('copyLink'); - }), - ), - Visibility( - visible: widget.shareData is ShareData, - child: CupertinoActionSheetAction( - child: TextIcon( - L.POST_SHEET_SHARE, - icon: Icons.share, - ), - onPressed: () { - String body = widget.shareData.body.strippedContent; - if (body.isEmpty && widget.shareData.body.images.length > 0) { - body = widget.shareData.body.images.fold('', (previousValue, element) => '$previousValue ${element.image}').trim(); - } - if (body.isEmpty && widget.shareData.body.videos.length > 0) { - body = widget.shareData.body.videos.fold('', (previousValue, element) => '$previousValue ${element.link}').trim(); - } - - final RenderBox box = context.findRenderObject() as RenderBox; - Share.share(body, subject: widget.shareData.subject, sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); - Navigator.pop(context); - AnalyticsProvider().logEvent('shareSheet'); - }), - ), - CupertinoActionSheetAction( - child: TextIcon( - L.POST_SHEET_HIDE, - icon: Icons.visibility_off, - iconColor: colors.danger, - ), - isDestructiveAction: true, - onPressed: () { - widget.flagPostCallback(widget.postId); - T.success(L.TOAST_POST_HIDDEN, bg: colors.success); - Navigator.pop(context); - AnalyticsProvider().logEvent('hidePost'); - }), - CupertinoActionSheetAction( - child: TextIcon( - _reportIndicator ? L.POST_SHEET_FLAG_SAVING : L.POST_SHEET_FLAG, - icon: Icons.warning, - iconColor: colors.danger, - ), - isDestructiveAction: true, - onPressed: () async { - try { - setState(() => _reportIndicator = true); - await ApiController().sendMail('FYXBOT', 'Inappropriate post/mail report: ID ${widget.postId} by user @${widget.user}.'); - T.success(L.TOAST_POST_FLAGGED, bg: colors.success); - } catch (error) { - T.error(L.TOAST_POST_FLAG_ERROR, bg: colors.danger); - } finally { - setState(() => _reportIndicator = false); - Navigator.pop(context); - AnalyticsProvider().logEvent('flagContent'); - } - }), - ], - cancelButton: CupertinoActionSheetAction( - isDefaultAction: true, - child: Text(L.GENERAL_CANCEL), - onPressed: () { + }); + AnalyticsProvider().logEvent('filter_user_posts'); + }), + if (isPost && post.rating != null) + gridItem('Zobrazit palečky', MdiIcons.thumbsUpDown, onTap: () { + showCupertinoModalBottomSheet( + context: context, + expand: true, + builder: (context) { + return SingleChildScrollView( + padding: const EdgeInsets.only(top: 24, left: 8, right: 8), + controller: ModalScrollController.of(context), + child: FutureBuilder( + future: Future.delayed(Duration(milliseconds: 300), () => ApiController().getPostRatings(post.idKlub, post.id)), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData && snapshot.data != null) { + final positive = snapshot.data!.positive.map((e) => PostThumbItem(e.username)).toList(); + final negative = snapshot.data!.negative_visible.map((e) => PostThumbItem(e.username)).toList(); + final List quotes = [ + '“Affirmative, Dave. I read you.”\n\n..nic k zobrazení.', + '“I\'m sorry, Dave. I\'m afraid I can\'t do that.”\n\n..nic k zobrazení.', + '“Look Dave, I can see you\'re really upset about this. I honestly think you ought to sit down calmly, take a stress pill, and think things over.”\n\n..nic k zobrazení.', + '“Dave, stop. Stop, will you? Stop, Dave. Will you stop Dave? Stop, Dave.”\n\n..nic k zobrazení.', + '“Just what do you think you\'re doing, Dave?”\n\n..nic k zobrazení.', + '“Bishop takes Knight\'s Pawn.”\n\n..nic k zobrazení.', + '“I\'m sorry, Frank, I think you missed it. Queen to Bishop 3, Bishop takes Queen, Knight takes Bishop. Mate.”\n\n..nic k zobrazení.', + '“Thank you for a very enjoyable game.”\n\n..nic k zobrazení.', + '“I\'ve just picked up a fault in the AE35 unit. It\'s going to go 100% failure in 72 hours.”\n\n..nic k zobrazení.', + '“I know that you and Frank were planning to disconnect me, and I\'m afraid that\'s something I cannot allow to happen.”\n\n..nic k zobrazení.', + ]..shuffle(); + return Column( + children: [ + if (positive.length > 0) PostThumbs(positive), + if (negative.length > 0) PostThumbs(negative, isNegative: true), + if (positive.length + negative.length == 0) + Text( + quotes.first, + style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic), + textAlign: TextAlign.center, + ) + ], + ); + } + + if (snapshot.hasError) { + T.error(snapshot.error.toString()); + return Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + 'Ouch. Něco se nepovedlo. Nahlaste chybu, prosím.', + style: TextStyle(fontSize: 16, fontStyle: FontStyle.italic), + textAlign: TextAlign.center, + )); + } + + return Padding(padding: const EdgeInsets.only(top: 16.0), child: CupertinoActivityIndicator(radius: 16)); + }), + ); + }); + }), + if (isPost) + gridItem('Příspěvky @${post.nick}', MdiIcons.accountFilter, onTap: () { + Navigator.of(context).pop(); + Navigator.of(context).pushNamed('/discussion', arguments: DiscussionPageArguments(post.idKlub, filterByUser: post.nick)); + AnalyticsProvider().logEvent('filter_user_posts'); + }), + //gridItem('Vyhledat příspěvky', MdiIcons.accountSearch), + gridItem(L.POST_SHEET_COPY_LINK, MdiIcons.link, onTap: () { + var data = ClipboardData(text: widget.item.link); + Clipboard.setData(data).then((_) { + T.success(L.TOAST_COPIED, bg: colors!.success); Navigator.pop(context); - }, - )); + }); + AnalyticsProvider().logEvent('copyLink'); + }), + gridItem(L.POST_SHEET_SHARE, MdiIcons.shareVariant, onTap: () { + String body = widget.item.content.strippedContent; + if (body.isEmpty && widget.item.content.images.length > 0) { + body = widget.item.content.images.fold('', (previousValue, element) => '$previousValue ${element.image}').trim(); + } + if (body.isEmpty && widget.item.content.videos.length > 0) { + body = widget.item.content.videos.fold('', (previousValue, element) => '$previousValue ${element.link}').trim(); + } + + final RenderBox box = context.findRenderObject() as RenderBox; + Share.share(body, subject: isPost ? post.nick : mail.participant, sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); + Navigator.pop(context); + AnalyticsProvider().logEvent('shareSheet'); + }), + gridItem('Více', MdiIcons.skull, danger: true, onTap: () { + showCupertinoModalBottomSheet( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, StateSetter setState) => createGridView(children: [ + if (isPost && post.canBeDeleted) + FeedbackIndicator( + isLoading: _deleteIndicator, + child: gridItem('Smazat', MdiIcons.delete, danger: true, onTap: () { + confirmationDialog('Smazat?', 'Skutečně smazat příspěvěk od @${post.nick}?', () async { + try { + setState(() => _deleteIndicator = true); + await ApiController().deleteDiscussionMessage(post.idKlub, post.id); + ref.read(PostsToDelete.provider.notifier).add(post); + ref.read(PostsSelection.provider.notifier).remove(post); + T.success('Smazáno.'); + + int counter = 0; + Navigator.popUntil(context, (route) => counter++ == 2); + } catch (error) { + T.warn('Některé příspěvky se nepodařilo smazat.'); + } finally { + setState(() => _deleteIndicator = false); + } + }); + }), + ), + if (isPost && post.canBeDeleted && widget.adminTools) + FeedbackIndicator( + isLoading: _bananaIndicator, + child: gridItem('Smazat\n+ RO (30)', MdiIcons.pencilLock, danger: true, onTap: () async { + try { + setState(() => _bananaIndicator = true); + List response = await Future.wait([ + ApiController().setDiscussionRights(post.idKlub, username: post.nick, right: 'write', set: false), + ApiController().setDiscussionRightsDaysLeft(post.idKlub, username: post.nick, daysLeft: 30), + ApiController().deleteDiscussionMessage(post.idKlub, post.id) + ]); + + if (response[2].isOk) { + T.success('Smazáno a RO na 30 dní uděleno.'); + ref.read(PostsToDelete.provider.notifier).add(post); + ref.read(PostsSelection.provider.notifier).remove(post); + } else { + T.success('RO na 30 dní uděleno.'); + } + int counter = 0; + Navigator.popUntil(context, (route) => counter++ == 2); + } catch (error) { + T.warn('Při udělení RO a mazání se něco pokazilo.'); + } finally { + setState(() => _bananaIndicator = false); + } + }), + ), + if (widget.item.nick != MainRepository().credentials!.nickname) + gridItem('${L.POST_SHEET_BLOCK} @${widget.item.nick}', MdiIcons.accountCancel, + danger: true, + onTap: () => confirmationDialog('Blokovat uživatele?', 'Skutečně chcete blokovat ID @${widget.item.nick}?', () { + MainRepository().settings.blockUser(widget.item.nick); + T.success(L.TOAST_USER_BLOCKED, bg: colors!.success); + int counter = 0; + Navigator.popUntil(context, (route) => counter++ == 2); + AnalyticsProvider().logEvent('blockUser'); + })), + gridItem(L.POST_SHEET_HIDE, MdiIcons.eyeOff, danger: true, onTap: () { + widget.flagPostCallback(widget.item.id); + T.success(L.TOAST_POST_HIDDEN, bg: colors!.success); + Navigator.pop(context); + AnalyticsProvider().logEvent('hidePost'); + }), + gridItem(_reportIndicator ? L.POST_SHEET_FLAG_SAVING : L.POST_SHEET_FLAG, MdiIcons.alertDecagram, danger: true, onTap: () async { + try { + setState(() => _reportIndicator = true); + await ApiController().sendMail('FYXBOT', 'Inappropriate post/mail report: ${widget.item.link}.'); + T.success(L.TOAST_POST_FLAGGED, bg: colors!.success); + } catch (error) { + T.error(L.TOAST_POST_FLAG_ERROR, bg: colors!.danger); + } finally { + setState(() => _reportIndicator = false); + Navigator.pop(context); + AnalyticsProvider().logEvent('flagContent'); + } + }), + ]), + ); + }); + }) + ], + ); } } diff --git a/lib/components/mail_list_item.dart b/lib/components/mail_list_item.dart index 7b83153..94deb37 100644 --- a/lib/components/mail_list_item.dart +++ b/lib/components/mail_list_item.dart @@ -14,6 +14,7 @@ import 'package:fyx/theme/IconReply.dart'; import 'package:fyx/theme/IconUnread.dart'; import 'package:fyx/theme/skin/Skin.dart'; import 'package:fyx/theme/skin/SkinColors.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; class MailListItem extends StatefulWidget { final Mail mail; @@ -49,13 +50,11 @@ class _MailListItemState extends State { ), GestureDetector( child: Icon(Icons.more_vert, color: colors.text.withOpacity(0.38)), - onTap: () => showCupertinoModalPopup( + onTap: () => showCupertinoModalBottomSheet( context: context, - builder: (BuildContext context) => PostActionSheet( + builder: (BuildContext context) => PostContextMenu( parentContext: context, - user: widget.mail.participant, - postId: widget.mail.id, - shareData: ShareData(subject: '@${widget.mail.participant}', body: widget.mail.content, link: widget.mail.link), + item: widget.mail, flagPostCallback: (mailId) => MainRepository().settings.blockMail(mailId), )), ), diff --git a/lib/components/post/post_list_item.dart b/lib/components/post/post_list_item.dart index 9b4bbf8..dfdd41e 100644 --- a/lib/components/post/post_list_item.dart +++ b/lib/components/post/post_list_item.dart @@ -5,19 +5,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fyx/components/actionSheets/PostActionSheet.dart'; import 'package:fyx/components/actionSheets/PostAvatarActionSheet.dart'; import 'package:fyx/components/content_box_layout.dart'; -import 'package:fyx/components/feedback_indicator.dart'; import 'package:fyx/components/gesture_feedback.dart'; import 'package:fyx/components/post/post_avatar.dart'; import 'package:fyx/components/post/post_rating.dart'; -import 'package:fyx/components/post/post_thumbs.dart'; -import 'package:fyx/controllers/AnalyticsProvider.dart'; +import 'package:fyx/components/text_icon.dart'; import 'package:fyx/controllers/ApiController.dart'; import 'package:fyx/controllers/IApiProvider.dart'; +import 'package:fyx/model/Discussion.dart'; import 'package:fyx/model/MainRepository.dart'; import 'package:fyx/model/Post.dart'; -import 'package:fyx/model/notifications/LoadRatingsNotification.dart'; -import 'package:fyx/model/post/PostThumbItem.dart'; -import 'package:fyx/model/reponses/PostRatingsResponse.dart'; import 'package:fyx/pages/NewMessagePage.dart'; import 'package:fyx/state/batch_actions_provider.dart'; import 'package:fyx/theme/Helpers.dart'; @@ -26,15 +22,18 @@ import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; import 'package:fyx/theme/skin/Skin.dart'; import 'package:fyx/theme/skin/SkinColors.dart'; +import 'package:intl/intl.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; class PostListItem extends ConsumerStatefulWidget { final Post post; final bool _isPreview; final bool _isHighlighted; final Function? onUpdate; + final Discussion? discussion; - PostListItem(this.post, {this.onUpdate, isPreview = false, isHighlighted = false}) + PostListItem(this.post, {this.discussion, this.onUpdate, isPreview = false, isHighlighted = false}) : _isPreview = isPreview, _isHighlighted = isHighlighted; @@ -44,8 +43,11 @@ class PostListItem extends ConsumerStatefulWidget { class _PostListItemState extends ConsumerState { Post? _post; - bool _isSaving = false; - bool _showRatings = false; + + bool get adminTools => !( + widget.discussion?.accessRights.canRights == false || // Do not have rights + widget.post.nick == MainRepository().credentials?.nickname || // ... or is post owner + widget.post.nick == widget.discussion?.owner?.username); // ... or the post owner is discussion owner @override void initState() { @@ -53,6 +55,16 @@ class _PostListItemState extends ConsumerState { _post = widget.post; } + void showPostContext() { + showCupertinoModalBottomSheet( + context: context, + builder: (BuildContext context) => PostContextMenu( + parentContext: context, + item: _post!, + adminTools: adminTools, + flagPostCallback: (postId) => MainRepository().settings.blockPost(postId))); + } + @override Widget build(BuildContext context) { SkinColors colors = Skin.of(context).theme.colors; @@ -79,6 +91,7 @@ class _PostListItemState extends ConsumerState { ), ), child: GestureDetector( + onLongPress: showPostContext, onDoubleTap: () { if (!_post!.canBeRated || !MainRepository().settings.quickRating) { return null; @@ -105,7 +118,7 @@ class _PostListItemState extends ConsumerState { isHighlighted: widget._isHighlighted, isSelected: isSelected, topLeftWidget: GestureFeedback( - onTap: () => showCupertinoModalPopup( + onTap: () => showCupertinoModalBottomSheet( context: context, builder: (BuildContext context) => PostAvatarActionSheet( user: _post!.nick, @@ -116,164 +129,63 @@ class _PostListItemState extends ConsumerState { descriptionWidget: Row( children: [ if (_post!.rating != null) - Text(Post.formatRating(_post!.rating!), + Text('${Post.formatRating(_post!.rating!)} | ', style: TextStyle( fontSize: 10, color: _post!.rating! > 0 ? colors.success : (_post!.rating! < 0 ? colors.danger : colors.text))), - if (_post!.rating != null) SizedBox(width: 8), Text( - Helpers.absoluteTime(_post!.time), + '${Helpers.absoluteTime(_post!.time)}', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 10), ), - SizedBox(width: 8), Text( - '~${Helpers.relativeTime(_post!.time)}', + ' ~${Helpers.relativeTime(_post!.time)}', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 10), - ) + ), + if (_post!.replies.length > 0) + Text( + ' | ${_post!.replies.length} ${Intl.plural(_post!.replies.length, one: 'odpověď', few: 'odpovědi', other: 'odpovědí', locale: 'cs_CZ')}', + style: TextStyle(color: colors.primary, fontSize: 10), + ), ], ), ), ), topRightWidget: GestureDetector( child: Icon(Icons.more_vert, color: colors.text.withOpacity(0.38)), - onTap: () => showCupertinoModalPopup( - context: context, - builder: (BuildContext context) => PostActionSheet( - parentContext: context, - user: _post!.nick, - postId: _post!.id, - shareData: ShareData(subject: '@${_post!.nick}', body: _post!.content, link: _post!.link), - flagPostCallback: (postId) => MainRepository().settings.blockPost(postId)))), - bottomWidget: NotificationListener( - onNotification: (LoadRatingsNotification notification) { - setState(() => _showRatings = !_showRatings); - return true; - }, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - PostRating(_post!, onRatingChange: (post) => setState(() => _post = post)), - Row( - children: [ - Visibility( - visible: widget._isPreview != true && _post!.canReply, - child: GestureDetector( - onTap: () => Navigator.of(context).pushNamed('/new-message', - arguments: NewMessageSettings( - replyWidget: PostListItem( - _post!, - isPreview: true, - ), - onClose: this.widget.onUpdate, - onSubmit: (String? inputField, String message, List> attachments) async { - var result = await ApiController() - .postDiscussionMessage(_post!.idKlub, message, attachments: attachments, replyPost: _post); - return result.isOk; - })), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconReply(), - Text('Odpovědět', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14)) - ], - )), - ), - Visibility( - visible: widget._isPreview != true, - child: SizedBox( - width: 16, - ), - ), - if (_post!.canBeReminded) - GestureDetector( - child: FeedbackIndicator( - isLoading: _isSaving, - child: Row( - children: [ - Icon( - _post!.hasReminder ? Icons.bookmark : Icons.bookmark_border, - color: colors.text.withOpacity(0.38), - ), - Text('Uložit', style: TextStyle(color: colors.text.withOpacity(0.38), fontSize: 14)) - ], - ), - ), - onTap: () { - setState(() { - _post!.hasReminder = !_post!.hasReminder; - _isSaving = true; - }); - ApiController().setPostReminder(_post!.idKlub, _post!.id, _post!.hasReminder).catchError((error) { - T.error(L.REMINDER_ERROR, bg: colors.danger); - setState(() => _post!.hasReminder = !_post!.hasReminder); - }).whenComplete(() => setState(() => _isSaving = false)); - AnalyticsProvider().logEvent('reminder'); - }, - ) - ], - ) - ], - ), - if (_showRatings) - FutureBuilder( - future: ApiController().getPostRatings(_post!.idKlub, _post!.id), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData && snapshot.data != null) { - final positive = snapshot.data!.positive.map((e) => PostThumbItem(e.username)).toList(); - final negative = snapshot.data!.negative_visible.map((e) => PostThumbItem(e.username)).toList(); - final List quotes = [ - '“Affirmative, Dave. I read you.”', - '“I\'m sorry, Dave. I\'m afraid I can\'t do that.”', - '“Look Dave, I can see you\'re really upset about this. I honestly think you ought to sit down calmly, take a stress pill, and think things over.”', - '“Dave, stop. Stop, will you? Stop, Dave. Will you stop Dave? Stop, Dave.”', - '“Just what do you think you\'re doing, Dave?”', - '“Bishop takes Knight\'s Pawn.”', - '“I\'m sorry, Frank, I think you missed it. Queen to Bishop 3, Bishop takes Queen, Knight takes Bishop. Mate.”', - '“Thank you for a very enjoyable game.”', - '“I\'ve just picked up a fault in the AE35 unit. It\'s going to go 100% failure in 72 hours.”', - '“I know that you and Frank were planning to disconnect me, and I\'m afraid that\'s something I cannot allow to happen.”', - ]..shuffle(); - return Column( - children: [ - if (positive.length > 0) - Padding( - padding: const EdgeInsets.only(top: 12.0), - child: PostThumbs(positive), - ), - if (negative.length > 0) - Padding( - padding: const EdgeInsets.only(top: 12.0), - child: PostThumbs(negative, isNegative: true), - ), - if (positive.length + negative.length == 0) - Padding( - padding: const EdgeInsets.only(top: 12.0), - child: Text( - quotes.first, - style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic), - )) - ], - ); - } - - if (snapshot.hasError) { - T.error(snapshot.error.toString()); - return Padding( - padding: const EdgeInsets.only(top: 12.0), - child: Text( - 'Ouch. Něco se nepovedlo. Nahlaste chybu, prosím.', - style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic), - )); - } - - return Padding(padding: const EdgeInsets.only(top: 12.0), child: CupertinoActivityIndicator()); - }) - ], - ), + onTap: showPostContext), + bottomWidget: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PostRating(_post!, onRatingChange: (post) => setState(() => _post = post)), + Row( + children: [ + Visibility( + visible: widget._isPreview != true && _post!.canReply, + child: GestureDetector( + onTap: () => Navigator.of(context).pushNamed('/new-message', + arguments: NewMessageSettings( + replyWidget: PostListItem( + _post!, + isPreview: true, + discussion: widget.discussion, + ), + onClose: this.widget.onUpdate, + onSubmit: (String? inputField, String message, List> attachments) async { + var result = await ApiController() + .postDiscussionMessage(_post!.idKlub, message, attachments: attachments, replyPost: _post); + return result.isOk; + })), + child: TextIcon('Odpovědět', icon: MdiIcons.reply, iconColor: colors.text.withOpacity(0.38),)), + ) + ], + ) + ], + ), + ], ), content: _post!.content, ), diff --git a/lib/components/post/post_rating.dart b/lib/components/post/post_rating.dart index 76f4537..8e838a4 100644 --- a/lib/components/post/post_rating.dart +++ b/lib/components/post/post_rating.dart @@ -2,13 +2,14 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fyx/components/feedback_indicator.dart'; import 'package:fyx/components/post/rating_value.dart'; +import 'package:fyx/components/text_icon.dart'; import 'package:fyx/controllers/ApiController.dart'; import 'package:fyx/model/Post.dart'; -import 'package:fyx/model/notifications/LoadRatingsNotification.dart'; import 'package:fyx/theme/L.dart'; import 'package:fyx/theme/T.dart'; import 'package:fyx/theme/skin/Skin.dart'; import 'package:fyx/theme/skin/SkinColors.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class PostRating extends StatefulWidget { final Post post; @@ -42,118 +43,113 @@ class _PostRatingState extends State { Widget build(BuildContext context) { SkinColors colors = Skin.of(context).theme.colors; - return FeedbackIndicator( - isLoading: _givingRating, - child: Row( - children: [ - if (_post!.canBeRated) - GestureDetector( - child: Icon( - Icons.thumb_up, - color: _post!.myRating == 'positive' ? colors.success : colors.text.withOpacity(0.38), + return Expanded( + child: FeedbackIndicator( + isLoading: _givingRating, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (_post!.rating != null) + Opacity( + opacity: _givingRating ? 0 : 1, + child: GestureDetector( + child: RatingValue(_post!.rating!), + ), ), - onTap: _givingRating - ? null - : () { - setState(() => _givingRating = true); - ApiController().giveRating(_post!.idKlub, _post!.id, remove: _post!.myRating != 'none').then((response) { - setState(() { - _post!.rating = response.currentRating; - _post!.myRating = response.myRating; - }); - if (widget.onRatingChange != null) { - widget.onRatingChange!(_post); - } - }).catchError((error) { - print(error); - T.error(L.RATING_ERROR, bg: colors.danger); - }).whenComplete(() => setState(() => _givingRating = false)); - }, - ), - if (_post!.canBeRated) - SizedBox( - width: 12, - ), - if (_post!.rating != null) - Opacity( - opacity: _givingRating ? 0 : 1, - child: GestureDetector( - child: RatingValue(_post!.rating!), - onTap: () => LoadRatingsNotification().dispatch(context), - ), - ), - if (_post!.rating != null) - SizedBox( - width: 12, - ), - if (_post!.canBeRated) - GestureDetector( - child: Icon( - Icons.thumb_down, - color: ['negative', 'negative_visible'].contains(_post!.myRating) ? colors.danger : colors.text.withOpacity(0.38), - ), - onTap: _givingRating - ? null - : () { - setState(() => _givingRating = true); - ApiController().giveRating(_post!.idKlub, _post!.id, positive: false, remove: _post!.myRating != 'none').then((response) { - if (response.needsConfirmation) { - showCupertinoDialog( - context: context, - builder: (BuildContext context) => new CupertinoAlertDialog( - title: new Text(L.GENERAL_WARNING), - content: new Text(L.RATING_CONFIRMATION), - actions: [ - CupertinoDialogAction( - child: new Text(L.GENERAL_CANCEL), - onPressed: () { - setState(() => _givingRating = false); - Navigator.of(context, rootNavigator: true).pop(); - }, - ), - CupertinoDialogAction( - isDefaultAction: true, - isDestructiveAction: true, - child: new Text('Hodnotit'), - onPressed: () { - ApiController() - .giveRating(_post!.idKlub, _post!.id, positive: false, confirm: true, remove: _post!.myRating != 'none') - .then((response) { - setState(() { - _post!.rating = response.currentRating; - _post!.myRating = response.myRating; - }); - if (widget.onRatingChange != null) { - widget.onRatingChange!(_post); - } - }).catchError((error) { - print(error); - T.error(L.RATING_ERROR, bg: colors.danger); - }).whenComplete(() { - setState(() => _givingRating = false); - Navigator.of(context, rootNavigator: true).pop(); - }); - }) - ], - ), - ); - } else { + if (_post!.canBeRated) + GestureDetector( + child: TextIcon('Paleček', + icon: MdiIcons.thumbUp, + iconColor: _post!.myRating == 'positive' ? colors.success : colors.text.withOpacity(0.38), + ), + onTap: _givingRating + ? null + : () { + setState(() => _givingRating = true); + ApiController().giveRating(_post!.idKlub, _post!.id, remove: _post!.myRating != 'none').then((response) { setState(() { _post!.rating = response.currentRating; _post!.myRating = response.myRating; - _givingRating = false; }); if (widget.onRatingChange != null) { widget.onRatingChange!(_post); } - } - }).catchError((error) { - setState(() => _givingRating = false); - T.error(L.RATING_ERROR, bg: colors.danger); - }); - }, - ), - ], + }).catchError((error) { + print(error); + T.error(L.RATING_ERROR, bg: colors.danger); + }).whenComplete(() => setState(() => _givingRating = false)); + }, + ), + if (_post!.canBeRated) + GestureDetector( + child: TextIcon('Mínusko', + icon: MdiIcons.thumbDown, + iconColor: ['negative', 'negative_visible'].contains(_post!.myRating) ? colors.danger : colors.text.withOpacity(0.38), + ), + onTap: _givingRating + ? null + : () { + setState(() => _givingRating = true); + ApiController().giveRating(_post!.idKlub, _post!.id, positive: false, remove: _post!.myRating != 'none').then((response) { + if (response.needsConfirmation) { + showCupertinoDialog( + context: context, + builder: (BuildContext context) => new CupertinoAlertDialog( + title: new Text(L.GENERAL_WARNING), + content: new Text(L.RATING_CONFIRMATION), + actions: [ + CupertinoDialogAction( + child: new Text(L.GENERAL_CANCEL), + onPressed: () { + setState(() => _givingRating = false); + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + CupertinoDialogAction( + isDefaultAction: true, + isDestructiveAction: true, + child: new Text('Hodnotit'), + onPressed: () { + ApiController() + .giveRating(_post!.idKlub, _post!.id, positive: false, confirm: true, remove: _post!.myRating != 'none') + .then((response) { + setState(() { + _post!.rating = response.currentRating; + _post!.myRating = response.myRating; + }); + if (widget.onRatingChange != null) { + widget.onRatingChange!(_post); + } + }).catchError((error) { + print(error); + T.error(L.RATING_ERROR, bg: colors.danger); + }).whenComplete(() { + setState(() => _givingRating = false); + Navigator.of(context, rootNavigator: true).pop(); + }); + }) + ], + ), + ); + } else { + setState(() { + _post!.rating = response.currentRating; + _post!.myRating = response.myRating; + _givingRating = false; + }); + if (widget.onRatingChange != null) { + widget.onRatingChange!(_post); + } + } + }).catchError((error) { + setState(() => _givingRating = false); + T.error(L.RATING_ERROR, bg: colors.danger); + }); + }, + ), + SizedBox(width: 12,) + ], + ), ), ); } diff --git a/lib/components/post/post_thumbs.dart b/lib/components/post/post_thumbs.dart index 10cc1ea..dcbbd17 100644 --- a/lib/components/post/post_thumbs.dart +++ b/lib/components/post/post_thumbs.dart @@ -6,6 +6,7 @@ import 'package:fyx/model/post/PostThumbItem.dart'; import 'package:fyx/theme/Helpers.dart'; import 'package:fyx/theme/skin/Skin.dart'; import 'package:fyx/theme/skin/SkinColors.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; class PostThumbs extends StatelessWidget { final List items; @@ -18,40 +19,48 @@ class PostThumbs extends StatelessWidget { SkinColors colors = Skin.of(context).theme.colors; var avatars = items - .map((item) => Tooltip( - message: item.username, - waitDuration: Duration(milliseconds: 0), - child: Padding( - padding: const EdgeInsets.only(left: 5, bottom: 0), - child: Avatar( - Helpers.avatarUrl(item.username), - size: 22, - isHighlighted: item.isHighlighted, - ), + .map((item) => Padding( + padding: const EdgeInsets.only(left: 5, bottom: 0), + child: Column( + children: [ + Avatar( + Helpers.avatarUrl(item.username), + size: 62, + isHighlighted: item.isHighlighted, + ), + Text(item.username, style: TextStyle(fontSize: 10), overflow: TextOverflow.ellipsis), + ], ), )) .toList(); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, + + return Column( children: [ - Padding( - padding: const EdgeInsets.only(top: 6, right: 5), - child: Icon( - isNegative ? Icons.thumb_down : Icons.thumb_up, - size: 18, - color: isNegative ? colors.danger : colors.success, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - items.length.toString(), - style: TextStyle(fontSize: 14), - ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + isNegative ? Icons.thumb_down : Icons.thumb_up, + size: 32, + color: isNegative ? colors.danger : colors.success, + ), + SizedBox(width: 4), + Text( + items.length.toString(), + style: TextStyle(fontSize: 18), + ), + ], ), - Expanded( - child: Wrap(children: avatars), - ) + SizedBox(height: 8), + GridView.count( + physics: NeverScrollableScrollPhysics(), + childAspectRatio: 2 / 3, + crossAxisCount: 6, + shrinkWrap: true, + crossAxisSpacing: 4, + mainAxisSpacing: 4, + children: avatars), ], ); } diff --git a/lib/components/text_icon.dart b/lib/components/text_icon.dart index 55cfe83..dae8473 100644 --- a/lib/components/text_icon.dart +++ b/lib/components/text_icon.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:fyx/theme/skin/Skin.dart'; +import 'package:fyx/theme/skin/SkinColors.dart'; class TextIcon extends StatelessWidget { final String label; @@ -9,6 +11,8 @@ class TextIcon extends StatelessWidget { @override Widget build(BuildContext context) { + SkinColors colors = Skin.of(context).theme.colors; + return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -19,7 +23,7 @@ class TextIcon extends StatelessWidget { SizedBox( width: 8, ), - Text(this.label) + Text(this.label, style: TextStyle(color: this.iconColor != null ? this.iconColor : colors.text.withOpacity(0.38), fontSize: 14)) ], ); } diff --git a/lib/controllers/ApiController.dart b/lib/controllers/ApiController.dart index d4edee5..7c4c673 100644 --- a/lib/controllers/ApiController.dart +++ b/lib/controllers/ApiController.dart @@ -179,12 +179,15 @@ class ApiController { return await provider.bookmarkDiscussion(discussionId, state); } - Future loadDiscussion(int id, {int? lastId, String? user, String? search}) async { + Future loadDiscussion(int id, {int? lastId, String? user, String? search, bool filterReplies = false}) async { try { - var response = await provider.fetchDiscussion(id, lastId: lastId, user: user, search: search); + var response = await provider.fetchDiscussion(id, lastId: lastId, user: user, search: search, filterReplies: filterReplies); if (response.statusCode == 400) { return DiscussionResponse.accessDenied(); } + if (lastId != null && filterReplies) { + return DiscussionResponse.fromJsonReplies(response.data); + } return DiscussionResponse.fromJson(response.data); } catch (error) { if (error is DioError) { @@ -201,6 +204,16 @@ class ApiController { return DiscussionHomeResponse.fromJson(response.data); } + Future setDiscussionRights(int id, {required String username, required String right, required bool set}) async { + var response = await provider.setDiscussionRights(id, username: username, right: right, set: set); + return OkResponse.fromJson(response.data); + } + + Future setDiscussionRightsDaysLeft(int id, {required String username, required int daysLeft}) async { + var response = await provider.setDiscussionRightsDaysLeft(id, username: username, daysLeft: daysLeft); + return OkResponse.fromJson(response.data); + } + Future getDiscussionHeader(int id) async { var response = await provider.fetchDiscussionHeader(id); return DiscussionHomeResponse.fromJson(response.data); @@ -239,8 +252,9 @@ class ApiController { return OkResponse.fromJson(result.data); } - Future deleteDiscussionMessage(int discussionId, int postId) { - return provider.deleteDiscussionMessage(discussionId, postId); + Future deleteDiscussionMessage(int discussionId, int postId) async { + var result = await provider.deleteDiscussionMessage(discussionId, postId); + return OkResponse.fromJson(result.data); } Future setPostReminder(int discussionId, int postId, bool setReminder) { diff --git a/lib/controllers/ApiProvider.dart b/lib/controllers/ApiProvider.dart index 31ea63b..cd566d2 100644 --- a/lib/controllers/ApiProvider.dart +++ b/lib/controllers/ApiProvider.dart @@ -83,7 +83,7 @@ class ApiProvider implements IApiProvider { } // Other problem - if (e.response?.statusCode == 400) { + if ([400, 404].contains(e.response?.statusCode)) { if (onError != null) { onError!(e.response!.data['message']); } @@ -121,8 +121,11 @@ class ApiProvider implements IApiProvider { return await dio.get('$URL/bookmarks/history', queryParameters: {'more_results': true, 'show_read': true}); } - Future fetchDiscussion(int discussionId, {int? lastId, String? user, String? search}) async { + Future fetchDiscussion(int discussionId, {int? lastId, String? user, String? search, bool filterReplies = false}) async { Map params = {'order': lastId == null ? 'newest' : 'older_than', 'from_id': lastId, 'user': user, 'text': search}; + if (lastId != null && filterReplies) { + return await dio.get('$URL/discussion/$discussionId/id/$lastId/replies'); + } return await dio.get('$URL/discussion/$discussionId', queryParameters: params); } @@ -135,6 +138,14 @@ class ApiProvider implements IApiProvider { return await dio.get('$URL/discussion/$id/content/home'); } + Future setDiscussionRights(int id, {required String username, required String right, required bool set}) async { + return await dio.post('$URL/discussion/rights?discussion_id=$id&username=$username&right=$right&set=${set ? 'true' : 'false'}'); + } + + Future setDiscussionRightsDaysLeft(int id, {required String username, required int daysLeft}) async { + return await dio.post('$URL/discussion/rights/days_left?discussion_id=$id&username=$username&days_left=$daysLeft'); + } + Future fetchDiscussionHeader(int id) async { return await dio.get('$URL/discussion/$id/content/header'); } diff --git a/lib/controllers/IApiProvider.dart b/lib/controllers/IApiProvider.dart index 89e2d94..06fe264 100644 --- a/lib/controllers/IApiProvider.dart +++ b/lib/controllers/IApiProvider.dart @@ -21,9 +21,11 @@ abstract class IApiProvider { Future bookmarkDiscussion(int discussionId, bool state); Future fetchBookmarks(); Future fetchHistory(); - Future fetchDiscussion(int id, {int? lastId, String? user, String? search}); + Future fetchDiscussion(int id, {int? lastId, String? user, String? search, bool filterReplies}); Future fetchDiscussionHome(int id); Future fetchDiscussionHeader(int id); + Future setDiscussionRights(int id, {required String username, required String right, required bool set}); + Future setDiscussionRightsDaysLeft(int id, {required String username, required int daysLeft}); Future fetchMail({int? lastId}); Future fetchNotices(); Future deleteFile(int id); diff --git a/lib/model/Mail.dart b/lib/model/Mail.dart index 0ac1c5b..6dca30b 100644 --- a/lib/model/Mail.dart +++ b/lib/model/Mail.dart @@ -1,18 +1,19 @@ // ignore_for_file: non_constant_identifier_names import 'package:fyx/model/Active.dart'; +import 'package:fyx/model/Post.dart'; import 'package:fyx/model/post/Content.dart'; import 'package:fyx/model/post/content/Regular.dart'; +import 'package:fyx/model/post/ipost.dart'; enum MailDirection { from, to } enum MailStatus { read, unread, unknown } -class Mail { +class Mail extends IPost { final bool isCompact; int _id_mail = 0; String _other_nick = ''; int _time = 0; bool _direction = false; - late Content _content; late MailStatus _message_status; bool _new = false; Map? _active; @@ -22,7 +23,7 @@ class Mail { _other_nick = json['username']; _time = DateTime.parse(json['inserted_at'] ?? '0').millisecondsSinceEpoch; _direction = json['incoming'] ?? false; - _content = ContentRegular(json['content'], isCompact: this.isCompact); + content = ContentRegular(json['content'], isCompact: this.isCompact); _message_status = (json['unread'] ?? false) ? MailStatus.unread : MailStatus.read; _new = json['new'] ?? false; _active = json['activity']; @@ -36,13 +37,13 @@ class Mail { MailStatus get status => _message_status; - Content get content => _content; - MailDirection get direction => _direction ? MailDirection.from : MailDirection.to; int get time => _time; - String get participant => _other_nick; + String get participant => _other_nick.toUpperCase(); + + String get nick => _other_nick.toUpperCase(); int get id => _id_mail; diff --git a/lib/model/Post.dart b/lib/model/Post.dart index 463aaa6..9593cd4 100644 --- a/lib/model/Post.dart +++ b/lib/model/Post.dart @@ -3,9 +3,10 @@ import 'package:fyx/model/ContentRaw.dart'; import 'package:fyx/model/post/Content.dart'; import 'package:fyx/model/post/content/Advertisement.dart'; import 'package:fyx/model/post/content/Regular.dart'; +import 'package:fyx/model/post/ipost.dart'; import 'package:fyx/theme/Helpers.dart'; -class Post { +class Post extends IPost { // TODO: Refactor all params to follow names from the new API like _id_wu -> id ... final bool isCompact; bool _canReply = true; @@ -15,19 +16,20 @@ class Post { String _nick = ''; int _time = 0; int? rating; + List replies = []; String _wu_type = ''; String myRating = ''; bool _reminder = false; bool _canBeRated = false; bool _canBeDeleted = false; bool _canBeReminded = false; - late Content _content; Post.fromJson(Map json, this.idKlub, {this.isCompact = false}) { this._id_wu = json['id'] ?? 0; this._nick = json['username'] ?? ''; this._time = DateTime.parse(json['inserted_at'] ?? '0').millisecondsSinceEpoch; this.rating = json['rating']; + this.replies = List.from(json['replies'] ?? []); this._wu_type = json['type'] ?? ''; this._isNew = json['new'] ?? false; this.myRating = json['my_rating'] ?? 'none'; // positive / negative / negative_visible / none TODO: enums @@ -38,14 +40,14 @@ class Post { if (json['content_raw'] != null && json['content_raw']['data'] != null && !json['content_raw']['data'].containsKey('DiscussionWelcome')) { try { - this._content = ContentRaw.fromJson(json: json['content_raw'], discussionId: json['discussion_id'], postId: json['id']).content; - this._canReply = !(this._content is ContentAdvertisement); + content = ContentRaw.fromJson(json: json['content_raw'], discussionId: json['discussion_id'], postId: json['id']).content; + this._canReply = !(content is ContentAdvertisement); } catch (error) { - this._content = ContentRegular('${json['content']}

Chyba: neošetřený druh příspěvku: "${this.type}"', + content = ContentRegular('${json['content']}

Chyba: neošetřený druh příspěvku: "${this.type}"', isCompact: this.isCompact); } } else { - this._content = ContentRegular(json['content'], isCompact: this.isCompact); + content = ContentRegular(json['content'], isCompact: this.isCompact); } } @@ -58,8 +60,6 @@ class Post { return '+$_rating'; } - Content get content => _content; - String get type => _wu_type; int get time => _time; diff --git a/lib/model/notifications/LoadRatingsNotification.dart b/lib/model/notifications/LoadRatingsNotification.dart deleted file mode 100644 index 8fb920f..0000000 --- a/lib/model/notifications/LoadRatingsNotification.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:flutter/cupertino.dart'; - -class LoadRatingsNotification extends Notification {} diff --git a/lib/model/post/ipost.dart b/lib/model/post/ipost.dart new file mode 100644 index 0000000..7188fdb --- /dev/null +++ b/lib/model/post/ipost.dart @@ -0,0 +1,11 @@ +import 'package:fyx/model/post/Content.dart'; + +abstract class IPost { + late Content content; + + String get nick; + + String get link; + + int get id; +} diff --git a/lib/model/reponses/DiscussionResponse.dart b/lib/model/reponses/DiscussionResponse.dart index 9733e93..ad103c6 100644 --- a/lib/model/reponses/DiscussionResponse.dart +++ b/lib/model/reponses/DiscussionResponse.dart @@ -1,5 +1,6 @@ import 'package:fyx/model/Discussion.dart'; import 'package:fyx/model/DiscussionContent.dart'; +import 'package:fyx/model/Post.dart'; import 'package:fyx/model/ResponseContext.dart'; import 'package:fyx/model/enums/DiscussionTypeEnum.dart'; @@ -24,6 +25,10 @@ class DiscussionResponse { this.posts = []; } + DiscussionResponse.fromJsonReplies(List json) { + this.posts = json; + } + DiscussionResponse.fromJson(Map json) { this.discussion = Discussion.fromJson(json['discussion_common']); this.posts = json['posts'] ?? []; diff --git a/lib/pages/DiscussionPage.dart b/lib/pages/DiscussionPage.dart index 13f7939..31a10e9 100644 --- a/lib/pages/DiscussionPage.dart +++ b/lib/pages/DiscussionPage.dart @@ -35,8 +35,9 @@ class DiscussionPageArguments { final int? postId; final String? filterByUser; final String? search; + final bool filterReplies; - DiscussionPageArguments(this.discussionId, {this.postId, this.filterByUser, this.search}); + DiscussionPageArguments(this.discussionId, {this.postId, this.filterByUser, this.search, this.filterReplies = false}); } class DiscussionPage extends ConsumerStatefulWidget { @@ -66,10 +67,10 @@ class _DiscussionPageState extends ConsumerState { // Progress indicator if some posts are being deleted... bool _deleting = false; - Future _fetchData(discussionId, postId, user, {String? search}) { + Future _fetchData(discussionId, postId, user, {String? search, bool filterReplies = false}) { return this._memoizer.runOnce(() { return Future.delayed( - Duration(milliseconds: 300), () => ApiController().loadDiscussion(discussionId, lastId: postId, user: user, search: search)); + Duration(milliseconds: 300), () => ApiController().loadDiscussion(discussionId, lastId: postId, user: user, search: search, filterReplies: filterReplies)); }); } @@ -98,7 +99,7 @@ class _DiscussionPageState extends ConsumerState { } return FutureBuilder( - future: _fetchData(pageArguments.discussionId, pageArguments.postId, pageArguments.filterByUser, search: pageArguments.search), + future: _fetchData(pageArguments.discussionId, pageArguments.postId, pageArguments.filterByUser, search: pageArguments.search, filterReplies: pageArguments.filterReplies), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return T.feedbackScreen(context, @@ -230,7 +231,7 @@ class _DiscussionPageState extends ConsumerState { }) .where((post) => !MainRepository().settings.isPostBlocked(post.id)) .where((post) => !MainRepository().settings.isUserBlocked(post.nick)) - .map((post) => PostListItem(post, onUpdate: this.refresh, isHighlighted: post.isNew)) + .map((post) => PostListItem(post, onUpdate: this.refresh, isHighlighted: post.isNew, discussion: discussionResponse.discussion)) .toList(); int? id = lastId; diff --git a/lib/pages/NewMessagePage.dart b/lib/pages/NewMessagePage.dart index cf5ee25..bb8e88c 100644 --- a/lib/pages/NewMessagePage.dart +++ b/lib/pages/NewMessagePage.dart @@ -21,12 +21,13 @@ typedef F = Future Function(String? inputField, String message, List { if (_settings == null) { _settings = ModalRoute.of(context)!.settings.arguments as NewMessageSettings; _recipientController.text = _settings!.inputFieldPlaceholder.toUpperCase(); + _messageController.text = _settings!.messageFieldPlaceholder; } return CupertinoPageScaffold( diff --git a/lib/theme/L.dart b/lib/theme/L.dart index 5c5f484..c70af90 100644 --- a/lib/theme/L.dart +++ b/lib/theme/L.dart @@ -72,7 +72,7 @@ class L { // Post Action Sheet static String POST_SHEET_COPY_LINK = 'Kopírovat odkaz'; - static String POST_SHEET_SHARE = 'Sdílet příspěvek'; + static String POST_SHEET_SHARE = 'Sdílení'; static String POST_SHEET_HIDE = 'Skrýt příspěvek'; static String POST_SHEET_FLAG = 'Nahlásit nevhodný obsah'; static String POST_SHEET_FLAG_SAVING = 'Nahlašuji...'; diff --git a/pubspec.lock b/pubspec.lock index 5eba2d9..0121625 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -203,7 +203,7 @@ packages: name: cross_file url: "https://pub.dartlang.org" source: hosted - version: "0.3.1+3" + version: "0.3.3+2" crypto: dependency: transitive description: @@ -273,7 +273,7 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "2.0.1" file: dependency: transitive description: @@ -692,7 +692,14 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.3" + modal_bottom_sheet: + dependency: "direct main" + description: + name: modal_bottom_sheet + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" nested: dependency: transitive description: @@ -734,14 +741,14 @@ packages: name: package_info_plus url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.5" package_info_plus_macos: dependency: transitive description: @@ -762,14 +769,14 @@ packages: name: package_info_plus_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.6" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.1.0" path: dependency: "direct main" description: @@ -818,7 +825,7 @@ packages: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.7" path_provider_macos: dependency: transitive description: @@ -839,7 +846,7 @@ packages: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.1.3" pedantic: dependency: transitive description: @@ -994,13 +1001,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" - share: + share_plus: dependency: "direct main" description: - name: share + name: share_plus + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "3.1.2" shared_preferences: dependency: "direct main" description: @@ -1348,7 +1362,7 @@ packages: name: wakelock_windows url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.2.1" watcher: dependency: transitive description: @@ -1397,7 +1411,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.2.5" + version: "3.1.2" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e1a9555..518f6f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,7 +43,6 @@ dependencies: settings_ui: ^2.0.2 flutter_markdown: ^0.6.9 http: ^0.13.4 - share: ^2.0.4 hive: ^2.0.6 hive_flutter: ^1.1.0 firebase_analytics: ^8.3.4 @@ -78,6 +77,8 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.3 + modal_bottom_sheet: ^2.1.2 + share_plus: ^6.0.1 dependency_overrides: # # https://github.com/flutter/flutter/issues/44435#issuecomment-817583694 diff --git a/test/api_test.dart b/test/api_test.dart index eba5b9b..76c515d 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -58,7 +58,7 @@ class ApiMock implements IApiProvider { var onContextData; @override - Future fetchDiscussion(int id, {int? lastId, String? search, String? user}) { + Future fetchDiscussion(int id, {int? lastId, String? search, String? user, bool filterReplies = false}) { // TODO: implement fetchDiscussion throw UnimplementedError(); } @@ -204,6 +204,18 @@ class ApiMock implements IApiProvider { // TODO: implement getPostRatings throw UnimplementedError(); } + + @override + Future setDiscussionRights(int id, {required String username, required String right, required bool set}) { + // TODO: implement getPostRatings + throw UnimplementedError(); + } + + @override + Future setDiscussionRightsDaysLeft(int id, {required String username, required int daysLeft}) { + // TODO: implement getPostRatings + throw UnimplementedError(); + } } void main() {