From 6b36efe9fe45dd7955160d398afb5761d05dfa77 Mon Sep 17 00:00:00 2001 From: noumantahir Date: Wed, 7 Aug 2024 20:23:07 +0500 Subject: [PATCH 01/18] added terms texts --- assets/translations/en.json | 17 ++++++++++------- lib/core/locales/locale_text.dart | 5 +++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 6bb8bc5..ed22a53 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -36,11 +36,14 @@ "sm_posting_login_message": "You have successfully logged in with key", "sm_comment_publish_message": "Comment is published successfully", "sm_vote_success_message": "You have successfully voted the user", - "something_went_wrong":"Something went wrong", - "login_with_posting_key":"Login with posting key", - "please_enter_the_username":"Please enter the username", - "please_enter_the_posting_key":"Please enter the posting key", - "username":"username", - "posting_key":"posting key", - "no_threads_found":"No threads found" + "something_went_wrong": "Something went wrong", + "login_with_posting_key": "Login with posting key", + "please_enter_the_username": "Please enter the username", + "please_enter_the_posting_key": "Please enter the posting key", + "username": "username", + "posting_key": "posting key", + "no_threads_found": "No threads found", + "age_limit": "\u2022 Only accounts older than {} days allowed", + "interpretation_token": "\u2022 User HP based vote interpretation", + "max_choices": "\u2022 You may select upto {} choices" } \ No newline at end of file diff --git a/lib/core/locales/locale_text.dart b/lib/core/locales/locale_text.dart index 0f5033d..bb703ca 100644 --- a/lib/core/locales/locale_text.dart +++ b/lib/core/locales/locale_text.dart @@ -45,4 +45,9 @@ class LocaleText { static const String username = "username"; static const String postingKey = "posting_key"; static const String noThreadsFound = "no_threads_found"; + + //polls realted texts + static String ageLimit (int days) => "age_limit".tr(args: [days.toString()]); + static String get interpretationToken => "interpretation_token".tr(); + static String maxChoices (int choices) => "max_choices".tr(args: [choices.toString()]); } From 18e015618db15f85338f5f3aa0fa2e7b79dad822 Mon Sep 17 00:00:00 2001 From: noumantahir Date: Wed, 7 Aug 2024 20:24:12 +0500 Subject: [PATCH 02/18] upgraded json metadata model to support poll data --- .../thread_json_meta_data.dart | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/lib/features/threads/models/thread_feeds/thread_json_meta_data/thread_json_meta_data.dart b/lib/features/threads/models/thread_feeds/thread_json_meta_data/thread_json_meta_data.dart index 51f0c47..9f15c56 100644 --- a/lib/features/threads/models/thread_feeds/thread_json_meta_data/thread_json_meta_data.dart +++ b/lib/features/threads/models/thread_feeds/thread_json_meta_data/thread_json_meta_data.dart @@ -1,6 +1,81 @@ import 'package:waves/core/utilities/save_convert.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_json_meta_data/thread_json_meta_data_video.dart'; +enum ContentType implements Comparable { + poll(value: 'poll'), + general(value: 'general'); + + const ContentType({ + required this.value, + }); + + final String value; + + @override + int compareTo(ContentType other) => 0; +} + +enum PollPreferredInterpretation + implements Comparable { + tokens(value: 'tokens'), + numberOfVotes(value: 'number_of_votes'); + + const PollPreferredInterpretation({ + required this.value, + }); + + final String value; + + // Lookup map to find enum from string + static final Map _valueMap = { + 'tokens': PollPreferredInterpretation.tokens, + 'number_of_votes': PollPreferredInterpretation.numberOfVotes, + }; + + // Convert string to enum + static PollPreferredInterpretation? fromString(String? value) { + + if(value == null){ + return null; + } + + final result = _valueMap[value]; + if (result == null) { + throw ArgumentError('Unknown value: $value'); + } + return result; + } + + // Convert enum to string + String toShortString() { + return value; + } + + @override + int compareTo(PollPreferredInterpretation other) => 0; +} + +class PollFilters { + final int accountAge; + + // Constructor + PollFilters({required this.accountAge}); + + // Optional: Method to convert to a map if needed + Map toMap() { + return { + 'account_age': accountAge, + }; + } + + // Optional: Method to create a PollFilter from a map + factory PollFilters.fromMap(Map map) { + return PollFilters( + accountAge: map['account_age'] ?? 0, + ); + } +} + class ThreadJsonMetadata { final List? tags; final List? image; @@ -11,6 +86,18 @@ class ThreadJsonMetadata { final ThreadJsonVideo? video; final String? format; + final ContentType? contentType; + final double? version; + final String? question; + final PollPreferredInterpretation? preferredInterpretation; + final int? maxChoicesVoted; + final List? choices; + final PollFilters? filters; + final DateTime? endTime; + final bool? uiHideResUntilVoted; + final bool? voteChange; + final bool? hideVotes; + const ThreadJsonMetadata({ required this.tags, required this.image, @@ -20,6 +107,17 @@ class ThreadJsonMetadata { this.links = const [], this.users = const [], required this.video, + this.contentType, + this.preferredInterpretation, + this.version, + this.question, + this.maxChoicesVoted, + this.choices, + this.filters, + this.endTime, + this.voteChange, + this.hideVotes, + this.uiHideResUntilVoted, }); factory ThreadJsonMetadata.fromJson(Map? json) => @@ -33,6 +131,21 @@ class ThreadJsonMetadata { video: ThreadJsonVideo.fromJson( asMap(json, 'video'), ), + contentType: + (json?['content_type'] != null && json?['content_type'] == 'poll') + ? ContentType.poll + : ContentType.general, + + version: json?['version'] as double?, + question: json?['question'] as String?, + choices: asList(json, 'choices').map((e) => e.toString()).toList(), + filters: json?['filters'] != null ? PollFilters.fromMap(json?['filters']) : null, + maxChoicesVoted: json?['max_choices_voted'] as int? ?? 1, + endTime: json?['end_time'].runtimeType == int ? (DateTime.fromMillisecondsSinceEpoch(json?['end_time'] * 1000)) : null, + voteChange: json?['vote_change'] as bool?, + hideVotes: json?['hide_votes'] as bool?, + uiHideResUntilVoted: json?['ui_hide_res_until_voted'] as bool?, + preferredInterpretation: PollPreferredInterpretation.fromString(json?['preferred_interpretation']) ); Map toJson() { @@ -45,6 +158,17 @@ class ThreadJsonMetadata { 'app': app, 'video': video?.toJson(), 'format': format, + 'content_type':contentType!.value, + 'version':version, + 'question':question, + 'choices': choices, + 'filters':filters?.toMap(), + 'max_choices_voted':maxChoicesVoted, + 'end_time':endTime != null ? (endTime!.millisecondsSinceEpoch / 1000).ceil() : null, + 'vote_change':voteChange, + 'hide_votes':hideVotes, + 'ui_hide_res_until_voted':uiHideResUntilVoted, + 'preferred_interpretation':preferredInterpretation!.value }; } From 23b4fa0e20b56bc4b0407fb62e1588cdcc9ccef8 Mon Sep 17 00:00:00 2001 From: noumantahir Date: Wed, 7 Aug 2024 20:24:54 +0500 Subject: [PATCH 03/18] added drafts for poll post and poll header widgets --- .../widgets/post_poll/poll_header.dart | 67 +++++++++++++++++++ .../widgets/post_poll/post_poll.dart | 55 +++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_header.dart create mode 100644 lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart diff --git a/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_header.dart b/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_header.dart new file mode 100644 index 0000000..b91aa30 --- /dev/null +++ b/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_header.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:timeago/timeago.dart' as timeago; +import 'package:waves/core/locales/locale_text.dart'; +import 'package:waves/features/threads/models/thread_feeds/thread_json_meta_data/thread_json_meta_data.dart'; + +class PollHeader extends StatelessWidget { + const PollHeader({super.key, required this.meta}); + + final ThreadJsonMetadata meta; + + @override + Widget build(BuildContext context) { + + //compile terms list + List termWidgets = []; + TextStyle temsTextStyle = const TextStyle(fontSize: 12); + if (meta.filters!.accountAge > 0) { + String text = LocaleText.ageLimit(meta.filters!.accountAge); + termWidgets.add(Text(text, style: temsTextStyle)); + } + if (meta.preferredInterpretation == PollPreferredInterpretation.tokens) { + termWidgets.add(Text(LocaleText.interpretationToken, style: temsTextStyle)); + } + if (meta.maxChoicesVoted! > 1) { + String text = LocaleText.maxChoices(meta.maxChoicesVoted!); + termWidgets.add(Text(text, style: temsTextStyle)); + } + + String timeString = ""; + if(meta.endTime != null){ + timeString = meta.endTime!.isAfter(DateTime.now()) + ? "Ends in ${timeago.format(meta.endTime!, allowFromNow: true, locale: 'en_short')}" + : "Ended"; + } + + + return Container( + alignment: Alignment.topLeft, + child: Wrap( + spacing: 8.0, // Horizontal space between children + runSpacing: 4.0, //ivalent to align-items: center + children: [ + Text( + meta.question!, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Text( + timeString, + style: const TextStyle(fontSize: 12), + ), + const Gap(4), + const Icon( + Icons.access_time, // Clock icon + size: 18.0, // Size of the icon + color: Colors.white, // Color of the icon + ) + ]), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: termWidgets, + ) + ], + )); + } +} diff --git a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart new file mode 100644 index 0000000..ae4738a --- /dev/null +++ b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; +import 'package:flutter_polls/flutter_polls.dart'; +import 'package:waves/features/threads/models/thread_feeds/thread_json_meta_data/thread_json_meta_data.dart'; +import 'package:waves/features/threads/presentation/thread_feed/widgets/post_poll/poll_header.dart'; + +class PostPoll extends StatelessWidget { + const PostPoll({super.key, required this.item}); + + final ThreadFeedModel item; + + @override + Widget build(BuildContext context) { + ThreadJsonMetadata? meta = item.jsonMetadata; + + Future onVoted(PollOption option, int total) { + print('voted options $option'); + return Future.value(true); + } + + if (meta == null || meta.contentType != ContentType.poll) { + return Container(); + } + + + return Container( + margin: const EdgeInsets.only(top: 12), + child: FlutterPolls( + pollId: item.permlink, + onVoted: (pollOption, newTotalVotes) => + onVoted(pollOption, newTotalVotes), + pollTitle: PollHeader(meta: meta), + pollOptions: meta.choices!.map((e) { + return PollOption( + id: e, + title: + Text(e, maxLines: 2), + votes: 3); + }).toList(), + heightBetweenOptions: 16, + pollOptionsHeight: 40, + votedBackgroundColor: const Color(0xff2e3d51), + pollOptionsFillColor: const Color(0xff2e3d51), + leadingVotedProgessColor: const Color(0xff357ce6), + votedProgressColor: const Color(0xff526d91), + votedCheckmark: const Icon(Icons.check, color: Colors.white, size: 24), + )); + } +} + +class Option { + dynamic text; + Option(this.text); +} From 95d4f41bc0427095de6295a441597ef73d9cd533 Mon Sep 17 00:00:00 2001 From: noumantahir Date: Wed, 7 Aug 2024 20:25:41 +0500 Subject: [PATCH 04/18] added flutter polls package --- pubspec.lock | 24 ++++++++++++++++++++++++ pubspec.yaml | 1 + 2 files changed, 25 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index 4f9d1e4..48b2bbe 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -262,6 +262,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_hooks: + dependency: transitive + description: + name: flutter_hooks + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + url: "https://pub.dev" + source: hosted + version: "0.20.5" flutter_launcher_icons: dependency: "direct dev" description: @@ -299,6 +307,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.20" + flutter_polls: + dependency: "direct main" + description: + name: flutter_polls + sha256: "29bf74ea5096946c2861f76f06d65a690013b8aa599c21ff6baf747b045074c4" + url: "https://pub.dev" + source: hosted + version: "0.1.6" flutter_secure_storage: dependency: "direct main" description: @@ -645,6 +661,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + percent_indicator: + dependency: transitive + description: + name: percent_indicator + sha256: c37099ad833a883c9d71782321cb65c3a848c21b6939b6185f0ff6640d05814c + url: "https://pub.dev" + source: hosted + version: "4.2.3" petitparser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f1d4d8e..4a10088 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: package_info_plus: ^8.0.0 keyboard_actions: ^4.2.0 firebase_analytics: ^11.2.0 + flutter_polls: ^0.1.6 dev_dependencies: From c2e167b3f129b0c92e24b6415615d35d7120f601 Mon Sep 17 00:00:00 2001 From: noumantahir Date: Wed, 7 Aug 2024 20:27:48 +0500 Subject: [PATCH 05/18] using post poll widget inside thread tile --- .../threads/presentation/thread_feed/widgets/thread_tile.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/features/threads/presentation/thread_feed/widgets/thread_tile.dart b/lib/features/threads/presentation/thread_feed/widgets/thread_tile.dart index 540fcd5..ac79a69 100644 --- a/lib/features/threads/presentation/thread_feed/widgets/thread_tile.dart +++ b/lib/features/threads/presentation/thread_feed/widgets/thread_tile.dart @@ -4,8 +4,10 @@ import 'package:go_router/go_router.dart'; import 'package:waves/core/routes/routes.dart'; import 'package:waves/core/utilities/constants/ui_constants.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; +import 'package:waves/features/threads/models/thread_feeds/thread_json_meta_data/thread_json_meta_data.dart'; import 'package:waves/features/threads/presentation/thread_feed/widgets/interaction_tile.dart'; import 'package:waves/features/threads/presentation/thread_feed/widgets/markdown/thread_markdown.dart'; +import 'package:waves/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart'; import 'package:waves/features/threads/presentation/thread_feed/widgets/thread_user_info_tile.dart'; class ThreadTile extends StatelessWidget { @@ -37,6 +39,7 @@ class ThreadTile extends StatelessWidget { onTap: () => context.pushNamed(Routes.commentDetailView, extra: item), child: ThreadMarkDown(item: item)), + item.jsonMetadata?.contentType == ContentType.poll ? PostPoll(item: item) : Container() , const Gap(20), InteractionTile( item: item, From 8b45e8879638983f268e90763d5b4b02d18d0578 Mon Sep 17 00:00:00 2001 From: noumantahir Date: Fri, 9 Aug 2024 19:49:08 +0500 Subject: [PATCH 06/18] introduced poll api and controller --- lib/core/providers/global_providers.dart | 4 +- lib/core/services/poll_service/poll_api.dart | 45 +++ .../services/poll_service/poll_model.dart | 268 ++++++++++++++++++ .../controller/poll_controller.dart | 41 +++ 4 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 lib/core/services/poll_service/poll_api.dart create mode 100644 lib/core/services/poll_service/poll_model.dart create mode 100644 lib/features/threads/presentation/thread_feed/controller/poll_controller.dart diff --git a/lib/core/providers/global_providers.dart b/lib/core/providers/global_providers.dart index eebf975..986785d 100644 --- a/lib/core/providers/global_providers.dart +++ b/lib/core/providers/global_providers.dart @@ -1,6 +1,7 @@ import 'package:provider/single_child_widget.dart'; import 'package:provider/provider.dart'; import 'package:waves/core/utilities/theme/theme_mode.dart'; +import 'package:waves/features/threads/presentation/thread_feed/controller/poll_controller.dart'; import 'package:waves/features/threads/presentation/thread_feed/controller/thread_feed_controller.dart'; import 'package:waves/features/user/view/user_controller.dart'; @@ -23,6 +24,7 @@ class GlobalProviders { return previousThreadFeedController; } }, - ) + ), + ChangeNotifierProvider(create: (context) => PollController()) ]; } diff --git a/lib/core/services/poll_service/poll_api.dart b/lib/core/services/poll_service/poll_api.dart new file mode 100644 index 0000000..1b503c5 --- /dev/null +++ b/lib/core/services/poll_service/poll_api.dart @@ -0,0 +1,45 @@ + + +import 'package:http/http.dart' as http; +import 'package:waves/core/models/action_response.dart'; +import 'package:waves/core/services/poll_service/poll_model.dart'; +import 'package:waves/core/utilities/enum.dart'; + + +/// +/// swapper importable api url +/// https://polls-beta.hivehub.dev/ +/// +/// hive polls docs reference: +/// https://gitlab.com/peakd/hive-open-polls +/// + +Future> fetchPoll( + String author, + String permlink + ) async { + try { + var url = Uri.parse( + "https://polls.hivehub.dev/rpc/poll?author=eq.$author&permlink=eq.$permlink"); + + + + http.Response response = await http.get( + url, + ); + + if (response.statusCode == 200) { + return ActionSingleDataResponse( + data: PollModel.fromJsonString(response.body).first, + status: ResponseStatus.success, + isSuccess: true, + errorMessage: ""); + } else { + return ActionSingleDataResponse( + status: ResponseStatus.failed, errorMessage: "Server Error"); + } + } catch (e) { + return ActionSingleDataResponse( + status: ResponseStatus.failed, errorMessage: e.toString()); + } + } \ No newline at end of file diff --git a/lib/core/services/poll_service/poll_model.dart b/lib/core/services/poll_service/poll_model.dart new file mode 100644 index 0000000..104e296 --- /dev/null +++ b/lib/core/services/poll_service/poll_model.dart @@ -0,0 +1,268 @@ +import 'dart:convert'; + +import 'package:waves/core/utilities/enum.dart'; + +class PollModel { + final String author; + final DateTime created; + final String permlink; + final String parentPermlink; + final String parentAuthor; + final String? image; + final double protocolVersion; + final String question; + final String preferredInterpretation; + final String? token; + final DateTime endTime; + final String status; + final int maxChoicesVoted; + final int filterAccountAgeDays; + final bool uiHideResUntilVoted; + final String pollTrxId; + final List pollChoices; + final List pollVoters; + final PollStats pollStats; + + PollModel({ + required this.author, + required this.created, + required this.permlink, + required this.parentPermlink, + required this.parentAuthor, + this.image, + required this.protocolVersion, + required this.question, + required this.preferredInterpretation, + this.token, + required this.endTime, + required this.status, + required this.maxChoicesVoted, + required this.filterAccountAgeDays, + required this.uiHideResUntilVoted, + required this.pollTrxId, + required this.pollChoices, + required this.pollVoters, + required this.pollStats, + }); + + static List fromJsonString(String str) => + List.from(json.decode(str).map((x) => PollModel.fromJson(x))); + + factory PollModel.fromJson(Map json) { + return PollModel( + author: json['author'], + created: DateTime.parse(json['created']), + permlink: json['permlink'], + parentPermlink: json['parent_permlink'], + parentAuthor: json['parent_author'], + image: json['image'], + protocolVersion: json['protocol_version'].toDouble(), + question: json['question'], + preferredInterpretation: json['preferred_interpretation'], + token: json['token'], + endTime: DateTime.parse(json['end_time']), + status: json['status'], + maxChoicesVoted: json['max_choices_voted'], + filterAccountAgeDays: json['filter_account_age_days'], + uiHideResUntilVoted: json['ui_hide_res_until_voted'], + pollTrxId: json['poll_trx_id'], + pollChoices: (json['poll_choices'] as List) + .map((e) => PollChoice.fromJson(e as Map)) + .toList(), + pollVoters: (json['poll_voters'] as List) + .map((e) => PollVoter.fromJson(e as Map)) + .toList(), + pollStats: PollStats.fromJson(json['poll_stats'] as Map), + ); + } + + Map toJson() { + return { + 'author': author, + 'created': created.toIso8601String(), + 'permlink': permlink, + 'parent_permlink': parentPermlink, + 'parent_author': parentAuthor, + 'image': image, + 'protocol_version': protocolVersion, + 'question': question, + 'preferred_interpretation': preferredInterpretation, + 'token': token, + 'end_time': endTime.toIso8601String(), + 'status': status, + 'max_choices_voted': maxChoicesVoted, + 'filter_account_age_days': filterAccountAgeDays, + 'ui_hide_res_until_voted': uiHideResUntilVoted, + 'poll_trx_id': pollTrxId, + 'poll_choices': pollChoices.map((e) => e.toJson()).toList(), + 'poll_voters': pollVoters.map((e) => e.toJson()).toList(), + 'poll_stats': pollStats.toJson(), + }; + } +} + +class PollChoice { + final int choiceNum; + final String choiceText; + final Votes? votes; + + PollChoice({ + required this.choiceNum, + required this.choiceText, + this.votes, + }); + + factory PollChoice.fromJson(Map json) { + return PollChoice( + choiceNum: json['choice_num'], + choiceText: json['choice_text'], + votes: json['votes'] != null + ? Votes.fromJson(json['votes'] as Map) + : null, + ); + } + + static List fromValues(List values) { + return values + .asMap() + .entries + .map((entry) => PollChoice( + choiceNum: entry.key + 1, + choiceText: entry.value, + votes: null, + )) + .toList(); + } + + Map toJson() { + return { + 'choice_num': choiceNum, + 'choice_text': choiceText, + 'votes': votes?.toJson(), + }; + } +} + +class Votes { + final int totalVotes; + final double hiveHp; + final double hiveProxiedHp; + final double hiveHpInclProxied; + final double splSpsp; + final String? heToken; + + Votes({ + required this.totalVotes, + required this.hiveHp, + required this.hiveProxiedHp, + required this.hiveHpInclProxied, + required this.splSpsp, + this.heToken, + }); + + factory Votes.fromJson(Map json) { + return Votes( + totalVotes: json['total_votes'], + hiveHp: json['hive_hp'].toDouble(), + hiveProxiedHp: json['hive_proxied_hp'].toDouble(), + hiveHpInclProxied: json['hive_hp_incl_proxied'].toDouble(), + splSpsp: json['spl_spsp'].toDouble(), + heToken: json['he_token'], + ); + } + + Map toJson() { + return { + 'total_votes': totalVotes, + 'hive_hp': hiveHp, + 'hive_proxied_hp': hiveProxiedHp, + 'hive_hp_incl_proxied': hiveHpInclProxied, + 'spl_spsp': splSpsp, + 'he_token': heToken, + }; + } +} + +class PollVoter { + final String name; + final List choices; + final double hiveHp; + final String? heToken; + final double splSpsp; + final double hiveProxiedHp; + final double hiveHpInclProxied; + + PollVoter({ + required this.name, + required this.choices, + required this.hiveHp, + this.heToken, + required this.splSpsp, + required this.hiveProxiedHp, + required this.hiveHpInclProxied, + }); + + factory PollVoter.fromJson(Map json) { + return PollVoter( + name: json['name'], + choices: List.from(json['choices']), + hiveHp: json['hive_hp'].toDouble(), + heToken: json['he_token'], + splSpsp: json['spl_spsp'].toDouble(), + hiveProxiedHp: json['hive_proxied_hp'].toDouble(), + hiveHpInclProxied: json['hive_hp_incl_proxied'].toDouble(), + ); + } + + Map toJson() { + return { + 'name': name, + 'choices': choices, + 'hive_hp': hiveHp, + 'he_token': heToken, + 'spl_spsp': splSpsp, + 'hive_proxied_hp': hiveProxiedHp, + 'hive_hp_incl_proxied': hiveHpInclProxied, + }; + } +} + +class PollStats { + final int totalVotingAccountsNum; + final double totalHiveHp; + final double totalHiveProxiedHp; + final double totalHiveHpInclProxied; + final double totalSplSpsp; + final String? totalHeToken; + + PollStats({ + required this.totalVotingAccountsNum, + required this.totalHiveHp, + required this.totalHiveProxiedHp, + required this.totalHiveHpInclProxied, + required this.totalSplSpsp, + this.totalHeToken, + }); + + factory PollStats.fromJson(Map json) { + return PollStats( + totalVotingAccountsNum: json['total_voting_accounts_num'], + totalHiveHp: json['total_hive_hp'].toDouble(), + totalHiveProxiedHp: json['total_hive_proxied_hp'].toDouble(), + totalHiveHpInclProxied: json['total_hive_hp_incl_proxied'].toDouble(), + totalSplSpsp: json['total_spl_spsp'].toDouble(), + totalHeToken: json['total_he_token'], + ); + } + + Map toJson() { + return { + 'total_voting_accounts_num': totalVotingAccountsNum, + 'total_hive_hp': totalHiveHp, + 'total_hive_proxied_hp': totalHiveProxiedHp, + 'total_hive_hp_incl_proxied': totalHiveHpInclProxied, + 'total_spl_spsp': totalSplSpsp, + 'total_he_token': totalHeToken, + }; + } +} diff --git a/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart b/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart new file mode 100644 index 0000000..69e55cf --- /dev/null +++ b/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart @@ -0,0 +1,41 @@ + + +import 'package:flutter/material.dart'; +import 'package:waves/core/models/action_response.dart'; +import 'package:waves/core/services/poll_service/poll_api.dart'; +import 'package:waves/core/services/poll_service/poll_model.dart'; + +class PollController with ChangeNotifier { + bool _isLoading = false; + final Map _pollMap = {}; + String? _errorMessage; + + bool get isLoading => _isLoading; + String? get errorMessage => _errorMessage; + + Future fetchPollData(String author, String permlink) async { + _isLoading = true; + notifyListeners(); + + try { + // Simulate a network request + ActionSingleDataResponse response = await fetchPoll(author, permlink); + if(response.isSuccess && response.data != null){ + String pollKey = _getLocalPollKey(author, permlink); + _pollMap[pollKey] = response.data!; + } + } catch (error) { + _errorMessage = 'Error: $error'; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + PollModel? getPollData(String author, String permlink) => _pollMap[_getLocalPollKey(author, permlink)]; + + _getLocalPollKey (String author, String permlink){ + return "$author/$permlink"; + } + +} \ No newline at end of file From 67f36ad38b66bad955322ed9930474301dbf61ed Mon Sep 17 00:00:00 2001 From: noumantahir Date: Fri, 9 Aug 2024 19:50:00 +0500 Subject: [PATCH 07/18] fetching poll data and using in choices --- .../widgets/post_poll/post_poll.dart | 97 ++++++++++++++----- 1 file changed, 74 insertions(+), 23 deletions(-) diff --git a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart index ae4738a..2cc653f 100644 --- a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart +++ b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart @@ -1,18 +1,41 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; +import 'package:provider/provider.dart'; +import 'package:waves/core/services/poll_service/poll_model.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; import 'package:flutter_polls/flutter_polls.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_json_meta_data/thread_json_meta_data.dart'; +import 'package:waves/features/threads/presentation/thread_feed/controller/poll_controller.dart'; import 'package:waves/features/threads/presentation/thread_feed/widgets/post_poll/poll_header.dart'; -class PostPoll extends StatelessWidget { +class PostPoll extends StatefulWidget { const PostPoll({super.key, required this.item}); final ThreadFeedModel item; + @override + State createState() => _PostPollState(); +} + +class _PostPollState extends State { + @override + void initState() { + // TODO: implement initState + super.initState(); + + if (widget.item.jsonMetadata!.contentType == ContentType.poll) { + String author = widget.item.author, permlink = widget.item.permlink; + context.read().fetchPollData(author, permlink); + } + } + @override Widget build(BuildContext context) { - ThreadJsonMetadata? meta = item.jsonMetadata; + ThreadJsonMetadata? meta = widget.item.jsonMetadata; + String author = widget.item.author; + String permlink = widget.item.permlink; Future onVoted(PollOption option, int total) { print('voted options $option'); @@ -23,29 +46,57 @@ class PostPoll extends StatelessWidget { return Container(); } + List pollOptions() { + PollModel? poll = context.select( + (pollController) => pollController.getPollData(author, permlink)); + + List choices = + poll?.pollChoices ?? PollChoice.fromValues(meta.choices!); + + return choices + .map((e) => PollOption( + id: e.choiceNum.toString(), + title: Text(e.choiceText, maxLines: 2), + votes: e.votes?.totalVotes ?? 0)) + .toList(); + } return Container( - margin: const EdgeInsets.only(top: 12), - child: FlutterPolls( - pollId: item.permlink, - onVoted: (pollOption, newTotalVotes) => - onVoted(pollOption, newTotalVotes), - pollTitle: PollHeader(meta: meta), - pollOptions: meta.choices!.map((e) { - return PollOption( - id: e, - title: - Text(e, maxLines: 2), - votes: 3); - }).toList(), - heightBetweenOptions: 16, - pollOptionsHeight: 40, - votedBackgroundColor: const Color(0xff2e3d51), - pollOptionsFillColor: const Color(0xff2e3d51), - leadingVotedProgessColor: const Color(0xff357ce6), - votedProgressColor: const Color(0xff526d91), - votedCheckmark: const Icon(Icons.check, color: Colors.white, size: 24), - )); + margin: const EdgeInsets.only(top: 12), + child: Column( + children: [ + FlutterPolls( + pollId: widget.item.permlink, + onVoted: (pollOption, newTotalVotes) => + onVoted(pollOption, newTotalVotes), + pollTitle: PollHeader( + meta: meta, + ), + pollOptions: pollOptions(), + heightBetweenOptions: 16, + pollOptionsHeight: 40, + votedBackgroundColor: const Color(0xff2e3d51), + pollOptionsFillColor: const Color(0xff2e3d51), + leadingVotedProgessColor: const Color(0xff357ce6), + votedProgressColor: const Color(0xff526d91), + votedCheckmark: + const Icon(Icons.check, color: Colors.white, size: 24), + ), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + onPressed: () => print("button pressed"), + icon: const Icon(Icons.bar_chart), + label: const Text("Vote"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + padding: const EdgeInsets.symmetric(horizontal: 32) + ), + ), + ) + ], + ), + ); } } From c9736486c8e86eb08090100dc057f92f82840a4d Mon Sep 17 00:00:00 2001 From: noumantahir Date: Tue, 13 Aug 2024 20:10:22 +0500 Subject: [PATCH 08/18] removed flutter poll in favor of custom poll --- .../widgets/post_poll/poll_choices.dart | 401 ++++++++++++++++++ pubspec.lock | 12 +- pubspec.yaml | 3 +- 3 files changed, 405 insertions(+), 11 deletions(-) create mode 100644 lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart diff --git a/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart b/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart new file mode 100644 index 0000000..2fe8ec1 --- /dev/null +++ b/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart @@ -0,0 +1,401 @@ +library flutter_polls; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:percent_indicator/linear_percent_indicator.dart'; + +// FlutterPolls widget. +// This widget is used to display a poll. +// It can be used in any way and also in a [ListView] or [Column]. +class PollChoices extends HookWidget { + const PollChoices({ + super.key, + required this.pollId, + this.hasVoted = false, + this.userVotedOptionIds, + this.onVoted, + this.totalVotes = 0, + required this.onSelection, + required this.selectedIds, + this.loadingWidget, + required this.pollTitle, + this.heightBetweenTitleAndOptions = 10, + required this.pollOptions, + this.heightBetweenOptions, + this.votesText = 'Votes', + this.votesTextStyle, + this.metaWidget, + this.createdBy, + this.userToVote, + this.pollStartDate, + this.pollEnded = false, + this.pollOptionsHeight = 36, + this.pollOptionsWidth, + this.pollOptionsBorderRadius, + this.pollOptionsFillColor, + this.pollOptionsSplashColor = Colors.grey, + this.pollOptionsBorder, + this.votedPollOptionsBorder, + this.votedPollOptionsRadius, + this.votedBackgroundColor = const Color(0xffEEF0EB), + this.votedProgressColor = const Color(0xff84D2F6), + this.leadingVotedProgessColor = const Color(0xff0496FF), + this.voteInProgressColor = const Color(0xffEEF0EB), + this.votedCheckmark, + this.votedPercentageTextStyle, + this.votedAnimationDuration = 1000, + }) : _isloading = false; + + /// The id of the poll. + /// This id is used to identify the poll. + /// It is also used to check if a user has already voted in this poll. + final String? pollId; + + /// Checks if a user has already voted in this poll. + /// If this is set to true, the user can't vote in this poll. + /// Default value is false. + /// [userVotedOptionIds] must also be provided if this is set to true. + final bool hasVoted; + + /// Checks if the [onVoted] execution is completed or not + /// it is true, if the [onVoted] exection is ongoing and + /// false, if completed + final bool _isloading; + final double totalVotes; + + /// If a user has already voted in this poll. + /// It is ignored if [hasVoted] is set to false or not set at all. + final List? userVotedOptionIds; + + /// An asynchronous callback for HTTP call feature + /// Called when the user votes for an option. + /// The index of the option that the user voted for is passed as an argument. + /// If the user has already voted, this callback is not called. + /// If the user has not voted, this callback is called. + /// If the callback returns true, the tapped [PollOption] is considered as voted. + /// Else Nothing happens, + final Future Function(PollOption pollOption, int newTotalVotes)? onVoted; + + final Function(int choiceId, bool status) onSelection; + final Map selectedIds; + + /// The title of the poll. Can be any widget with a bounded size. + final Widget pollTitle; + + /// Data format for the poll options. + /// Must be a list of [PollOptionData] objects. + /// The list must have at least two elements. + /// The first element is the option that is selected by default. + /// The second element is the option that is selected by default. + /// The rest of the elements are the options that are available. + /// The list can have any number of elements. + /// + /// Poll options are displayed in the order they are in the list. + /// example: + /// + /// pollOptions = [ + /// + /// PollOption(id: 1, title: Text('Option 1'), votes: 2), + /// + /// PollOption(id: 2, title: Text('Option 2'), votes: 5), + /// + /// PollOption(id: 3, title: Text('Option 3'), votes: 9), + /// + /// PollOption(id: 4, title: Text('Option 4'), votes: 2), + /// + /// ] + /// + /// The [id] of each poll option is used to identify the option when the user votes. + /// The [title] of each poll option is displayed to the user. + /// [title] can be any widget with a bounded size. + /// The [votes] of each poll option is the number of votes that the option has received. + final List pollOptions; + + /// The height between the title and the options. + /// The default value is 10. + final double? heightBetweenTitleAndOptions; + + /// The height between the options. + /// The default value is 0. + final double? heightBetweenOptions; + + /// Votes text. Can be "Votes", "Votos", "Ibo" or whatever language. + /// If not specified, "Votes" is used. + final String? votesText; + + /// [votesTextStyle] is the text style of the votes text. + /// If not specified, the default text style is used. + /// Styles for [totalVotes] and [votesTextStyle]. + final TextStyle? votesTextStyle; + + /// [metaWidget] is displayed at the bottom of the poll. + /// It can be any widget with an unbounded size. + /// If not specified, no meta widget is displayed. + /// example: + /// metaWidget = Text('Created by: $createdBy') + final Widget? metaWidget; + + /// Who started the poll. + final String? createdBy; + + /// Current user about to vote. + final String? userToVote; + + /// The date the poll was created. + final DateTime? pollStartDate; + + /// If poll is closed. + final bool pollEnded; + + /// Height of a [PollOption]. + /// The height is the same for all options. + /// Defaults to 36. + final double? pollOptionsHeight; + + /// Width of a [PollOption]. + /// The width is the same for all options. + /// If not specified, the width is set to the width of the poll. + /// If the poll is not wide enough, the width is set to the width of the poll. + /// If the poll is too wide, the width is set to the width of the poll. + final double? pollOptionsWidth; + + /// Border radius of a [PollOption]. + /// The border radius is the same for all options. + /// Defaults to 0. + final BorderRadius? pollOptionsBorderRadius; + + /// Border of a [PollOption]. + /// The border is the same for all options. + /// Defaults to null. + /// If null, the border is not drawn. + final BoxBorder? pollOptionsBorder; + + /// Border of a [PollOption] when the user has voted. + /// The border is the same for all options. + /// Defaults to null. + /// If null, the border is not drawn. + final BoxBorder? votedPollOptionsBorder; + + /// Color of a [PollOption]. + /// The color is the same for all options. + /// Defaults to [Colors.blue]. + final Color? pollOptionsFillColor; + + /// Splashes a [PollOption] when the user taps it. + /// Defaults to [Colors.grey]. + final Color? pollOptionsSplashColor; + + /// Radius of the border of a [PollOption] when the user has voted. + /// Defaults to Radius.circular(8). + final Radius? votedPollOptionsRadius; + + /// Color of the background of a [PollOption] when the user has voted. + /// Defaults to [const Color(0xffEEF0EB)]. + final Color? votedBackgroundColor; + + /// Color of the progress bar of a [PollOption] when the user has voted. + /// Defaults to [const Color(0xff84D2F6)]. + final Color? votedProgressColor; + + /// Color of the leading progress bar of a [PollOption] when the user has voted. + /// Defaults to [const Color(0xff0496FF)]. + final Color? leadingVotedProgessColor; + + /// Color of the background of a [PollOption] when the user clicks to vote and its still in progress. + /// Defaults to [const Color(0xffEEF0EB)]. + final Color? voteInProgressColor; + + /// Widget for the checkmark of a [PollOption] when the user has voted. + /// Defaults to [Icons.check_circle_outline_rounded]. + final Widget? votedCheckmark; + + /// TextStyle of the percentage of a [PollOption] when the user has voted. + final TextStyle? votedPercentageTextStyle; + + /// Animation duration of the progress bar of the [PollOption]'s when the user has voted. + /// Defaults to 1000 milliseconds. + /// If the animation duration is too short, the progress bar will not animate. + /// If you don't want the progress bar to animate, set this to 0. + final int votedAnimationDuration; + + /// Loading animation widget for [PollOption] when [onVoted] callback is invoked + /// Defaults to [CircularProgressIndicator] + /// Visible until the [onVoted] execution is completed, + final Widget? loadingWidget; + + @override + Widget build(BuildContext context) { + + + // final votedOption = useState(hasVoted == false + // ? null + // : pollOptions + // .where( + // (pollOption) => userVotedOptionIds?.contains(pollOption.id) ?? false, + // ) + // .toList() + // .first); + + // final totalVotes = useState(pollOptions.fold( + // 0, + // (acc, option) => acc + option.votes, + // )); + + // useEffect(() { + // totalVotes.value = pollOptions.fold( + // 0, + // (acc, option) => acc + option.votes, + // ); + // return; + // }, [pollOptions]); + + // totalVotes.value = totalVotes.value; + + return Column( + key: ValueKey(pollId), + children: [ + pollTitle, + SizedBox(height: heightBetweenTitleAndOptions), + if (pollOptions.length < 2) + throw ('>>>Flutter Polls: Poll must have at least 2 options.<<<') + else + ...pollOptions.map( + (pollOption) { + if (hasVoted && userVotedOptionIds == null) { + throw ('>>>Flutter Polls: User has voted but [userVotedOptionId] is null.<<<'); + } else { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: hasVoted || pollEnded + ? Container( + key: UniqueKey(), + margin: EdgeInsets.only( + bottom: heightBetweenOptions ?? 8, + ), + decoration: votedPollOptionsBorder != null + ? BoxDecoration( + border: votedPollOptionsBorder, + borderRadius: BorderRadius.all( + votedPollOptionsRadius ?? + const Radius.circular(8), + ), + ) + : null, + child: LinearPercentIndicator( + width: pollOptionsWidth, + lineHeight: pollOptionsHeight!, + barRadius: votedPollOptionsRadius ?? + const Radius.circular(8), + padding: EdgeInsets.zero, + percent: totalVotes == 0 + ? 0 + : pollOption.votes / totalVotes, + animation: true, + animationDuration: votedAnimationDuration, + backgroundColor: votedBackgroundColor, + progressColor: + (userVotedOptionIds?.contains(pollOption.id) ?? false) + ? leadingVotedProgessColor + : votedProgressColor, + center: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 14, + ), + child: Row( + children: [ + Checkbox.adaptive( + value: (userVotedOptionIds?.contains(pollOption.id) ?? false), + onChanged: null), + pollOption.title, + const SizedBox(width: 10), + const Spacer(), + Text( + pollOption.votes.toString(), + style: votedPercentageTextStyle, + ), + ], + ), + ), + ), + ) + : Container( + key: UniqueKey(), + decoration: (selectedIds[pollOption.id] ?? false) ? BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + width: 4, + color: Colors.blue + ) + ) : null, + margin: EdgeInsets.only( + bottom: heightBetweenOptions ?? 8, + ), + child: InkWell( + onTap: () async { + // Disables clicking while loading + if (_isloading) return; + + bool preVal = selectedIds[pollOption.id] ?? false; + + onSelection( + pollOption.id, + !preVal + ); + + }, + splashColor: pollOptionsSplashColor, + borderRadius: pollOptionsBorderRadius ?? + BorderRadius.circular( + 8, + ), + child: Container( + height: pollOptionsHeight, + width: pollOptionsWidth, + padding: EdgeInsets.zero, + decoration: BoxDecoration( + color: (userVotedOptionIds?.contains(pollOption.id) ?? false) + ? voteInProgressColor + : pollOptionsFillColor, + border: pollOptionsBorder ?? + Border.all( + color: Colors.black, + width: 1, + ), + borderRadius: pollOptionsBorderRadius ?? + BorderRadius.circular( + 8, + ), + ), + child: Row( + children: [ + const Checkbox.adaptive( + value: false, onChanged: null), + pollOption.title + ], + ), + ), + ), + ), + ); + } + }, + ), + const SizedBox(height: 4), + ], + ); + } +} + +class PollOption { + PollOption({ + required this.id, + required this.title, + required this.votes, + }); + + final int id; + final Widget title; + int votes; +} diff --git a/pubspec.lock b/pubspec.lock index 48b2bbe..fc6cf95 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -263,7 +263,7 @@ packages: source: sdk version: "0.0.0" flutter_hooks: - dependency: transitive + dependency: "direct main" description: name: flutter_hooks sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 @@ -307,14 +307,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.20" - flutter_polls: - dependency: "direct main" - description: - name: flutter_polls - sha256: "29bf74ea5096946c2861f76f06d65a690013b8aa599c21ff6baf747b045074c4" - url: "https://pub.dev" - source: hosted - version: "0.1.6" flutter_secure_storage: dependency: "direct main" description: @@ -662,7 +654,7 @@ packages: source: hosted version: "2.3.0" percent_indicator: - dependency: transitive + dependency: "direct main" description: name: percent_indicator sha256: c37099ad833a883c9d71782321cb65c3a848c21b6939b6185f0ff6640d05814c diff --git a/pubspec.yaml b/pubspec.yaml index 4a10088..4e28805 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,7 +59,8 @@ dependencies: package_info_plus: ^8.0.0 keyboard_actions: ^4.2.0 firebase_analytics: ^11.2.0 - flutter_polls: ^0.1.6 + flutter_hooks: ^0.20.5 + percent_indicator: ^4.2.3 dev_dependencies: From 9ed895ab69ee284f192849dad0c8394d893b5e88 Mon Sep 17 00:00:00 2001 From: noumantahir Date: Tue, 13 Aug 2024 20:10:54 +0500 Subject: [PATCH 09/18] support for poll ending and poll selection state managing --- .../services/poll_service/poll_model.dart | 7 ++- .../widgets/post_poll/post_poll.dart | 61 +++++++++++++------ 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/lib/core/services/poll_service/poll_model.dart b/lib/core/services/poll_service/poll_model.dart index 104e296..8a02372 100644 --- a/lib/core/services/poll_service/poll_model.dart +++ b/lib/core/services/poll_service/poll_model.dart @@ -1,5 +1,5 @@ import 'dart:convert'; - +import 'package:collection/collection.dart'; import 'package:waves/core/utilities/enum.dart'; class PollModel { @@ -99,6 +99,11 @@ class PollModel { 'poll_stats': pollStats.toJson(), }; } + + List userVotedIds(String username) { + PollVoter? voter = pollVoters.firstWhereOrNull((item) => item.name == username); + return voter?.choices ?? []; // Filter for the correct name + } } class PollChoice { diff --git a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart index 2cc653f..9382375 100644 --- a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart +++ b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart @@ -1,13 +1,11 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:provider/provider.dart'; import 'package:waves/core/services/poll_service/poll_model.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; -import 'package:flutter_polls/flutter_polls.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_json_meta_data/thread_json_meta_data.dart'; import 'package:waves/features/threads/presentation/thread_feed/controller/poll_controller.dart'; +import 'package:waves/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart'; import 'package:waves/features/threads/presentation/thread_feed/widgets/post_poll/poll_header.dart'; class PostPoll extends StatefulWidget { @@ -20,6 +18,9 @@ class PostPoll extends StatefulWidget { } class _PostPollState extends State { + Map selection = {}; + bool hasVoted = false; + @override void initState() { // TODO: implement initState @@ -37,9 +38,33 @@ class _PostPollState extends State { String author = widget.item.author; String permlink = widget.item.permlink; - Future onVoted(PollOption option, int total) { - print('voted options $option'); - return Future.value(true); + PollModel? poll = context.select( + (pollController) => pollController.getPollData(author, permlink)); + + bool hasEnded = poll?.endTime.isBefore(DateTime.now()) ?? false; + + bool voteEnabled = poll != null && + !hasEnded && + !hasVoted && + selection.entries + .fold(false, (prevVal, entry) => entry.value || prevVal); + + onCastVote() async { + bool status = await Future.delayed(Duration(seconds: 2), () => true); + + if (status) { + setState(() { + hasVoted = true; + }); + } + + return status; + } + + onSelection(int id, bool value) { + setState(() { + selection = {...selection, id: value}; + }); } if (meta == null || meta.contentType != ContentType.poll) { @@ -47,15 +72,12 @@ class _PostPollState extends State { } List pollOptions() { - PollModel? poll = context.select( - (pollController) => pollController.getPollData(author, permlink)); - List choices = poll?.pollChoices ?? PollChoice.fromValues(meta.choices!); return choices .map((e) => PollOption( - id: e.choiceNum.toString(), + id: e.choiceNum, title: Text(e.choiceText, maxLines: 2), votes: e.votes?.totalVotes ?? 0)) .toList(); @@ -65,16 +87,20 @@ class _PostPollState extends State { margin: const EdgeInsets.only(top: 12), child: Column( children: [ - FlutterPolls( + PollChoices( pollId: widget.item.permlink, - onVoted: (pollOption, newTotalVotes) => - onVoted(pollOption, newTotalVotes), + onSelection: (id, status) => onSelection(id, status), pollTitle: PollHeader( meta: meta, ), pollOptions: pollOptions(), + selectedIds: selection, + pollEnded: hasEnded, + hasVoted: hasVoted, heightBetweenOptions: 16, pollOptionsHeight: 40, + userVotedOptionIds: poll?.userVotedIds("tahir"), + totalVotes: poll?.pollStats.totalVotingAccountsNum.toDouble() ?? 0, votedBackgroundColor: const Color(0xff2e3d51), pollOptionsFillColor: const Color(0xff2e3d51), leadingVotedProgessColor: const Color(0xff357ce6), @@ -85,13 +111,12 @@ class _PostPollState extends State { Align( alignment: Alignment.centerRight, child: ElevatedButton.icon( - onPressed: () => print("button pressed"), + onPressed: voteEnabled ? () => onCastVote() : null, icon: const Icon(Icons.bar_chart), label: const Text("Vote"), style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - padding: const EdgeInsets.symmetric(horizontal: 32) - ), + backgroundColor: Colors.blue, + padding: const EdgeInsets.symmetric(horizontal: 32)), ), ) ], From 13f302cddd7a0106e56747e073222f58af28e68d Mon Sep 17 00:00:00 2001 From: noumantahir Date: Thu, 15 Aug 2024 17:24:20 +0500 Subject: [PATCH 10/18] access to user data from poll controller --- lib/core/providers/global_providers.dart | 12 +++++++++++- .../thread_feed/controller/poll_controller.dart | 10 ++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/core/providers/global_providers.dart b/lib/core/providers/global_providers.dart index 986785d..d6ad530 100644 --- a/lib/core/providers/global_providers.dart +++ b/lib/core/providers/global_providers.dart @@ -25,6 +25,16 @@ class GlobalProviders { } }, ), - ChangeNotifierProvider(create: (context) => PollController()) + ChangeNotifierProxyProvider( + create: (context) => PollController(userData: null), + update: (context, userController, prevPollController) { + if (prevPollController == null) { + return PollController(userData: userController.userData); + } else { + prevPollController.updateUserData(userController.userData); + return prevPollController; + } + }, + ) ]; } diff --git a/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart b/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart index 69e55cf..0ce7dff 100644 --- a/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart +++ b/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart @@ -4,15 +4,25 @@ import 'package:flutter/material.dart'; import 'package:waves/core/models/action_response.dart'; import 'package:waves/core/services/poll_service/poll_api.dart'; import 'package:waves/core/services/poll_service/poll_model.dart'; +import 'package:waves/features/auth/models/user_auth_model.dart'; class PollController with ChangeNotifier { + UserAuthModel? userData; + bool _isLoading = false; final Map _pollMap = {}; + String? _errorMessage; bool get isLoading => _isLoading; String? get errorMessage => _errorMessage; + PollController({required this.userData}); + + updateUserData(UserAuthModel? user) { + userData = user; + } + Future fetchPollData(String author, String permlink) async { _isLoading = true; notifyListeners(); From adbf29a9aba0a8a3cc1d86ca0fd04af750df91ec Mon Sep 17 00:00:00 2001 From: noumantahir Date: Thu, 15 Aug 2024 17:24:47 +0500 Subject: [PATCH 11/18] poll cache inject draft --- .../services/poll_service/poll_model.dart | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/core/services/poll_service/poll_model.dart b/lib/core/services/poll_service/poll_model.dart index 8a02372..bbd571b 100644 --- a/lib/core/services/poll_service/poll_model.dart +++ b/lib/core/services/poll_service/poll_model.dart @@ -19,8 +19,8 @@ class PollModel { final int filterAccountAgeDays; final bool uiHideResUntilVoted; final String pollTrxId; - final List pollChoices; - final List pollVoters; + List pollChoices; + List pollVoters; final PollStats pollStats; PollModel({ @@ -100,10 +100,30 @@ class PollModel { }; } - List userVotedIds(String username) { - PollVoter? voter = pollVoters.firstWhereOrNull((item) => item.name == username); + List userVotedIds(String? username) { + if (username == null) { + return []; + } + PollVoter? voter = + pollVoters.firstWhereOrNull((item) => item.name == username); return voter?.choices ?? []; // Filter for the correct name } + + + void injectPollVoteCache(String username, List selection) { + + pollVoters.retainWhere((entry) => entry.name != username); + + PollVoter voter = PollVoter( + name: username, + choices: selection, + hiveHp: 0, splSpsp: 0, hiveProxiedHp: 0, hiveHpInclProxied: 0); + + pollVoters = [ + ...pollVoters, + voter, + ]; + } } class PollChoice { From bffd5221439a6370e891a348aeb5ba284dbe0d2e Mon Sep 17 00:00:00 2001 From: noumantahir Date: Thu, 15 Aug 2024 17:25:04 +0500 Subject: [PATCH 12/18] draft for cast poll --- .../controller/poll_controller.dart | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart b/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart index 0ce7dff..bd31e68 100644 --- a/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart +++ b/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart @@ -1,5 +1,3 @@ - - import 'package:flutter/material.dart'; import 'package:waves/core/models/action_response.dart'; import 'package:waves/core/services/poll_service/poll_api.dart'; @@ -29,10 +27,14 @@ class PollController with ChangeNotifier { try { // Simulate a network request - ActionSingleDataResponse response = await fetchPoll(author, permlink); - if(response.isSuccess && response.data != null){ + ActionSingleDataResponse response = + await fetchPoll(author, permlink); + if (response.isSuccess && response.data != null) { String pollKey = _getLocalPollKey(author, permlink); - _pollMap[pollKey] = response.data!; + PollModel? data = response.data; + if (data != null) { + _pollMap[pollKey] = data; + } } } catch (error) { _errorMessage = 'Error: $error'; @@ -42,10 +44,40 @@ class PollController with ChangeNotifier { } } - PollModel? getPollData(String author, String permlink) => _pollMap[_getLocalPollKey(author, permlink)]; + Future castVote( + String author, String permlink, List selection) async { + print("cast vote for $author $permlink using choice $selection"); - _getLocalPollKey (String author, String permlink){ - return "$author/$permlink"; + String pollKey = _getLocalPollKey(author, permlink); + PollModel? poll = _pollMap[pollKey]; + + if (userData == null) { + return false; + } + + if (poll == null) { + return false; + } + + bool status = await Future.delayed(const Duration(seconds: 2), () => true); + + if (status) { + //TOOD: update poll model with update vote + poll.injectPollVoteCache(userData!.accountName, selection); + notifyListeners(); + } + return status; } -} \ No newline at end of file + List userVotedIds(String author, String permlink) => + _pollMap[_getLocalPollKey(author, permlink)] + ?.userVotedIds(userData!.accountName) ?? + []; + + PollModel? getPollData(String author, String permlink) => + _pollMap[_getLocalPollKey(author, permlink)]; + + _getLocalPollKey(String author, String permlink) { + return "$author/$permlink"; + } +} From 83452351190d02170521f435b5ad548940227b88 Mon Sep 17 00:00:00 2001 From: noumantahir Date: Thu, 15 Aug 2024 17:25:55 +0500 Subject: [PATCH 13/18] support for showing vote results and showing voting indicator --- .../widgets/post_poll/poll_choices.dart | 4 +- .../widgets/post_poll/post_poll.dart | 82 +++++++++++++++---- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart b/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart index 2fe8ec1..fa5e4e6 100644 --- a/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart +++ b/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart @@ -355,9 +355,7 @@ class PollChoices extends HookWidget { width: pollOptionsWidth, padding: EdgeInsets.zero, decoration: BoxDecoration( - color: (userVotedOptionIds?.contains(pollOption.id) ?? false) - ? voteInProgressColor - : pollOptionsFillColor, + color: pollOptionsFillColor, border: pollOptionsBorder ?? Border.all( color: Colors.black, diff --git a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart index 9382375..83adfd6 100644 --- a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart +++ b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart @@ -2,11 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:provider/provider.dart'; import 'package:waves/core/services/poll_service/poll_model.dart'; +import 'package:waves/features/auth/presentation/controller/auth_controller.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_json_meta_data/thread_json_meta_data.dart'; import 'package:waves/features/threads/presentation/thread_feed/controller/poll_controller.dart'; import 'package:waves/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart'; import 'package:waves/features/threads/presentation/thread_feed/widgets/post_poll/poll_header.dart'; +import 'package:waves/features/user/view/user_controller.dart'; class PostPoll extends StatefulWidget { const PostPoll({super.key, required this.item}); @@ -19,7 +21,8 @@ class PostPoll extends StatefulWidget { class _PostPollState extends State { Map selection = {}; - bool hasVoted = false; + bool enableRevote = false; + bool isVoting = false; @override void initState() { @@ -38,27 +41,45 @@ class _PostPollState extends State { String author = widget.item.author; String permlink = widget.item.permlink; + String? username = context.select( + (userController) => userController.userName); PollModel? poll = context.select( (pollController) => pollController.getPollData(author, permlink)); bool hasEnded = poll?.endTime.isBefore(DateTime.now()) ?? false; + List userVotedIds = poll?.userVotedIds(username) ?? []; + bool hasVoted = userVotedIds.isNotEmpty; + bool voteEnabled = poll != null && !hasEnded && - !hasVoted && + (!hasVoted || enableRevote) && selection.entries .fold(false, (prevVal, entry) => entry.value || prevVal); onCastVote() async { - bool status = await Future.delayed(Duration(seconds: 2), () => true); + PollController pollController = context.read(); - if (status) { + if (poll?.pollTrxId != null && selection.isNotEmpty) { setState(() { - hasVoted = true; + isVoting = true; }); - } - return status; + List selectedIds = selection.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + + bool status = await pollController.castVote( + poll!.author, poll.permlink, selectedIds); + + if (status) { + setState(() { + enableRevote = false; + isVoting = false; + }); + } + } } onSelection(int id, bool value) { @@ -67,6 +88,12 @@ class _PostPollState extends State { }); } + onRevote() { + setState(() { + enableRevote = true; + }); + } + if (meta == null || meta.contentType != ContentType.poll) { return Container(); } @@ -96,10 +123,10 @@ class _PostPollState extends State { pollOptions: pollOptions(), selectedIds: selection, pollEnded: hasEnded, - hasVoted: hasVoted, + hasVoted: !enableRevote && hasVoted, heightBetweenOptions: 16, pollOptionsHeight: 40, - userVotedOptionIds: poll?.userVotedIds("tahir"), + userVotedOptionIds: userVotedIds, totalVotes: poll?.pollStats.totalVotingAccountsNum.toDouble() ?? 0, votedBackgroundColor: const Color(0xff2e3d51), pollOptionsFillColor: const Color(0xff2e3d51), @@ -109,14 +136,35 @@ class _PostPollState extends State { const Icon(Icons.check, color: Colors.white, size: 24), ), Align( - alignment: Alignment.centerRight, - child: ElevatedButton.icon( - onPressed: voteEnabled ? () => onCastVote() : null, - icon: const Icon(Icons.bar_chart), - label: const Text("Vote"), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - padding: const EdgeInsets.symmetric(horizontal: 32)), + alignment: Alignment.centerLeft, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (hasVoted && !enableRevote) + TextButton( + onPressed: () => onRevote(), + child: const Text("Revote"), + ), + ElevatedButton.icon( + onPressed: voteEnabled ? () => onCastVote() : null, + icon: isVoting + ? Container( + width: 24.0, + height: 24.0, + padding: const EdgeInsets.all(4.0), + child: const CircularProgressIndicator( + valueColor: + AlwaysStoppedAnimation(Colors.white), + strokeWidth: 2, + ), + ) + : const Icon(Icons.bar_chart), + label: const Text("Vote"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + padding: const EdgeInsets.symmetric(horizontal: 32)), + ), + ], ), ) ], From 898dfe8c7d4036ea1856a7474300048758e024a8 Mon Sep 17 00:00:00 2001 From: noumantahir Date: Thu, 15 Aug 2024 17:35:06 +0500 Subject: [PATCH 14/18] improved choice selection presentation --- .../thread_feed/widgets/post_poll/poll_choices.dart | 12 +++--------- .../thread_feed/widgets/post_poll/post_poll.dart | 1 + 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart b/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart index fa5e4e6..581a9ed 100644 --- a/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart +++ b/lib/features/threads/presentation/thread_feed/widgets/post_poll/poll_choices.dart @@ -322,13 +322,7 @@ class PollChoices extends HookWidget { ) : Container( key: UniqueKey(), - decoration: (selectedIds[pollOption.id] ?? false) ? BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - width: 4, - color: Colors.blue - ) - ) : null, + margin: EdgeInsets.only( bottom: heightBetweenOptions ?? 8, ), @@ -358,8 +352,8 @@ class PollChoices extends HookWidget { color: pollOptionsFillColor, border: pollOptionsBorder ?? Border.all( - color: Colors.black, - width: 1, + color: (selectedIds[pollOption.id] ?? false) ? Colors.blue : pollOptionsFillColor!, + width: 2, ), borderRadius: pollOptionsBorderRadius ?? BorderRadius.circular( diff --git a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart index 83adfd6..c470cea 100644 --- a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart +++ b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart @@ -77,6 +77,7 @@ class _PostPollState extends State { setState(() { enableRevote = false; isVoting = false; + selection = {}; }); } } From 4da1ef3c4dc4c83bd66f5a734e5c1909026de108 Mon Sep 17 00:00:00 2001 From: noumantahir Date: Thu, 15 Aug 2024 18:16:02 +0500 Subject: [PATCH 15/18] migrated poll cache injection code from main ecency app to dart --- .../services/poll_service/poll_model.dart | 91 +++++++++++++++++-- 1 file changed, 83 insertions(+), 8 deletions(-) diff --git a/lib/core/services/poll_service/poll_model.dart b/lib/core/services/poll_service/poll_model.dart index bbd571b..0a4fa01 100644 --- a/lib/core/services/poll_service/poll_model.dart +++ b/lib/core/services/poll_service/poll_model.dart @@ -109,19 +109,73 @@ class PollModel { return voter?.choices ?? []; // Filter for the correct name } - void injectPollVoteCache(String username, List selection) { - pollVoters.retainWhere((entry) => entry.name != username); + double userHp = 1; //TOOD: use real user hp + + //extract previously votes choices and new choices + + final existingVote = + pollVoters.firstWhereOrNull((pv) => pv.name == username); + final previousUserChoices = pollChoices + .where((pc) => existingVote?.choices.contains(pc.choiceNum) ?? false) + .toList(); + final selectedChoices = + pollChoices.where((pc) => selection.contains(pc.choiceNum)).toList(); + + // // filtered list to separate untoched multiple choice e.g from old [1,2,3] new [3,4,5], removed would be [1, 2] , new would be [4, 5] + final removedChoices = previousUserChoices + .where( + (pc) => !selectedChoices.any((sc) => sc.choiceNum == pc.choiceNum)) + .toList(); + final newChoices = selectedChoices + .where((pc) => + !previousUserChoices.any((opc) => opc.choiceNum == pc.choiceNum)) + .toList(); + + // // votes that were not affected by new vote + + final notTouchedChoices = pollChoices + .where((pc) => + !removedChoices.any((rc) => rc.choiceNum == pc.choiceNum) && + !newChoices.any((nc) => nc.choiceNum == pc.choiceNum)) + .toList(); - PollVoter voter = PollVoter( - name: username, - choices: selection, - hiveHp: 0, splSpsp: 0, hiveProxiedHp: 0, hiveHpInclProxied: 0); + final otherVoters = pollVoters.where((pv) => pv.name != username).toList(); + //aggregate update poll choices list + pollChoices = [ + ...notTouchedChoices, + ...removedChoices.map((pc) => pc.copyWith( + votes: Votes( + totalVotes: (pc.votes?.totalVotes ?? 0) - 1, + hiveHp: (pc.votes?.hiveHp ?? 0) - userHp, + hiveProxiedHp: 0, + hiveHpInclProxied: (pc.votes?.hiveHpInclProxied ?? 0) - userHp, + splSpsp: 0, + ), + )), + ...newChoices.map((pc) => pc.copyWith( + votes: Votes( + totalVotes: (pc.votes?.totalVotes ?? 0) + 1, + hiveHp: (pc.votes?.hiveHp ?? 0) + userHp, + hiveProxiedHp: 0, + hiveHpInclProxied: (pc.votes?.hiveHpInclProxied ?? 0) + userHp, + splSpsp: 0, + ), + )), + ]..sort((a, b) => a.choiceNum.compareTo(b.choiceNum)); + + //update poll voters with updated selection pollVoters = [ - ...pollVoters, - voter, + ...otherVoters, + PollVoter( + name: username, + choices: selection, + hiveHp: userHp, + splSpsp: 0, + hiveProxiedHp: 0, + hiveHpInclProxied: userHp) ]; } } @@ -166,6 +220,15 @@ class PollChoice { 'votes': votes?.toJson(), }; } + + + PollChoice copyWith({Votes? votes}) { + return PollChoice( + choiceNum: choiceNum, + choiceText: choiceText, + votes: votes ?? this.votes + ); + } } class Votes { @@ -205,6 +268,18 @@ class Votes { 'spl_spsp': splSpsp, 'he_token': heToken, }; + } + + Votes copyWith({int? totalVotes, double? hiveHp, double? hiveHpInclProxied}) { + return Votes( + totalVotes: totalVotes ?? this.totalVotes, + hiveHp: hiveHp ?? this.hiveHp, + hiveProxiedHp: hiveProxiedHp, + hiveHpInclProxied: hiveHpInclProxied ?? this.hiveHpInclProxied, + splSpsp: splSpsp + ); + + } } From 99eb171dd62dbd4a640b566bec175502eccf6247 Mon Sep 17 00:00:00 2001 From: noumantahir Date: Thu, 15 Aug 2024 18:25:07 +0500 Subject: [PATCH 16/18] total votes aggregation --- lib/core/services/poll_service/poll_model.dart | 7 +++++++ .../thread_feed/widgets/post_poll/post_poll.dart | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/core/services/poll_service/poll_model.dart b/lib/core/services/poll_service/poll_model.dart index 0a4fa01..e2d4940 100644 --- a/lib/core/services/poll_service/poll_model.dart +++ b/lib/core/services/poll_service/poll_model.dart @@ -100,6 +100,13 @@ class PollModel { }; } + double get totalInterpretedVotes { + + //TODO: return value based on selected interpretation; + return pollChoices.fold(0, (val, entry) => val + (entry.votes?.totalVotes ?? 0)); + + } + List userVotedIds(String? username) { if (username == null) { return []; diff --git a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart index c470cea..2db5eec 100644 --- a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart +++ b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart @@ -128,7 +128,7 @@ class _PostPollState extends State { heightBetweenOptions: 16, pollOptionsHeight: 40, userVotedOptionIds: userVotedIds, - totalVotes: poll?.pollStats.totalVotingAccountsNum.toDouble() ?? 0, + totalVotes: poll?.totalInterpretedVotes ?? 0, votedBackgroundColor: const Color(0xff2e3d51), pollOptionsFillColor: const Color(0xff2e3d51), leadingVotedProgessColor: const Color(0xff357ce6), @@ -141,7 +141,7 @@ class _PostPollState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - if (hasVoted && !enableRevote) + if (hasVoted && !enableRevote && (meta.voteChange ?? false)) TextButton( onPressed: () => onRevote(), child: const Text("Revote"), From f10bbdd88a053af11e730fbe51e4f6aa24d725da Mon Sep 17 00:00:00 2001 From: noumantahir Date: Fri, 16 Aug 2024 18:25:46 +0500 Subject: [PATCH 17/18] added support for castpollvote broadcast --- android/app/src/main/assets/index.html | 36 ++++++++++++ .../kotlin/com/ecency/waves/MainActivity.kt | 8 +++ ios/Runner/AppBridge.swift | 16 +++++ ios/Runner/public/index.html | 33 +++++++++++ lib/core/models/broadcast_model.dart | 27 +++++++++ .../services/data_service/api_service.dart | 23 ++++++++ .../services/data_service/mobile_service.dart | 29 +++++++++- lib/core/services/data_service/service.dart | 11 ++++ .../services/data_service/web_service.dart | 11 ++++ .../comment/comment_navigation_model.dart | 4 ++ .../sign_transaction_hive_controller.dart | 47 ++++++++++----- ...gn_transaction_hive_signer_controller.dart | 26 +++++++++ ...gn_transaction_posting_key_controller.dart | 19 ++++++ .../controller/poll_controller.dart | 58 ++++++++++++++++--- .../widgets/post_poll/post_poll.dart | 18 +++--- .../threads/repository/thread_repository.dart | 15 +++++ 16 files changed, 346 insertions(+), 35 deletions(-) diff --git a/android/app/src/main/assets/index.html b/android/app/src/main/assets/index.html index cab950b..4e479b6 100644 --- a/android/app/src/main/assets/index.html +++ b/android/app/src/main/assets/index.html @@ -286,6 +286,42 @@ ); } + + //Add poll vote method here + function castPollVote( + id, + username, + pollId, + choices, + postingKey, + token, + authKey + ) { + let op = [ + "custom_json", + { + "id": "polls", + "required_auths": [], + "required_posting_auths": [ + username + ], + "json": JSON.stringify({ "poll": pollId, "action": "vote", "choices": choices }) + } + + ]; + performOperations( + id, + [op], + "polls", + username, + postingKey, + token, + authKey + ); + } + + + function muteUser( id, username, diff --git a/android/app/src/main/kotlin/com/ecency/waves/MainActivity.kt b/android/app/src/main/kotlin/com/ecency/waves/MainActivity.kt index 9fd5dc5..d20b043 100644 --- a/android/app/src/main/kotlin/com/ecency/waves/MainActivity.kt +++ b/android/app/src/main/kotlin/com/ecency/waves/MainActivity.kt @@ -98,6 +98,14 @@ class MainActivity: FlutterActivity() { "voteContent('$id','$username', '$author', '$permlink', '$weight', '$postingKey', '$token', '$authKey');", null ) + } + else if (call.method == "castPollVote" && username != null && pollId != null + && choices != null && postingKey != null && token != null + && authKey != null ) { + webView?.evaluateJavascript( + "castPollVote('$id','$username', '$pollId', '$choices', '$postingKey', '$token', '$authKey');", + null + ) } else if (call.method == "getImageUploadProofWithPostingKey" && username != null && postingKey != null) { webView?.evaluateJavascript( "getImageUploadProofWithPostingKey('$id', '$username', '$postingKey');", diff --git a/ios/Runner/AppBridge.swift b/ios/Runner/AppBridge.swift index 58e3eac..00df358 100644 --- a/ios/Runner/AppBridge.swift +++ b/ios/Runner/AppBridge.swift @@ -147,6 +147,22 @@ class AppBridge: NSObject { id: id, jsCode: "voteContent('\(id)','\(username)', '\(author)', '\(permlink)', '\(weight)', '\(postingKey)', '\(token)', '\(authKey)');" ) { text in result(text) } + case "castPollVote": + guard + let username = arguments ["username"] as? String, + let pollId = arguments ["pollId"] as? String, + let choices = arguments ["choices"] as? [Int], + let postingKey = arguments ["postingKey"] as? String, + let token = arguments ["token"] as? String, + let authKey = arguments ["authKey"] as? String + else { + debugPrint("username, pollId, choices, postingkey, token, authKey - are note set") + return result(FlutterMethodNotImplemented) + } + webVC.runThisJS( + id: id, + jsCode: "castPollVote('\(id)','\(username)', '\(pollId)', '\(choices)', '\(postingKey)', '\(token)', '\(authKey)');" + ) { text in result(text) } case "muteUser": guard let username = arguments ["username"] as? String, diff --git a/ios/Runner/public/index.html b/ios/Runner/public/index.html index 33d2f5f..89fccf0 100644 --- a/ios/Runner/public/index.html +++ b/ios/Runner/public/index.html @@ -265,6 +265,39 @@ ); } + //Add poll vote method here + function castPollVote( + id, + username, + pollId, + choices, + postingKey, + token, + authKey + ) { + let op = [ + "custom_json", + { + "id": "polls", + "required_auths": [], + "required_posting_auths": [ + username + ], + "json": JSON.stringify({ "poll": pollId, "action": "vote", "choices": choices }) + } + + ]; + performOperations( + id, + [op], + "polls", + username, + postingKey, + token, + authKey + ); + } + function muteUser( id, username, diff --git a/lib/core/models/broadcast_model.dart b/lib/core/models/broadcast_model.dart index bb543d6..c7ef506 100644 --- a/lib/core/models/broadcast_model.dart +++ b/lib/core/models/broadcast_model.dart @@ -46,6 +46,33 @@ class VoteBroadCastModel { } } +class PollVoteBroadcastModel { + final String username; + final String pollId; + final List choices; + + const PollVoteBroadcastModel({ + required this.username, + required this.pollId, + required this.choices + }); + + + Map toJson() { + return { + "id": "polls", + "required_posting_auths": [username], + "json": json.encode( + { + "poll": pollId, + "action": "vote", + "choices": choices + } + ) + }; + } +} + class CommentBroadCastModel { final String parentAuthor; final String parentPermlink; diff --git a/lib/core/services/data_service/api_service.dart b/lib/core/services/data_service/api_service.dart index 1938c76..b22a384 100644 --- a/lib/core/services/data_service/api_service.dart +++ b/lib/core/services/data_service/api_service.dart @@ -206,6 +206,29 @@ class ApiService { } } + + Future> castPollVote( + String username, + String pollId, + List choices, + String? postingKey, + String? authKey, + String? token, + ) async { + try { + String jsonString = await castPollVoteFromPlatform( + username, pollId, choices, postingKey, authKey, token); + ActionSingleDataResponse response = + ActionSingleDataResponse.fromJsonString(jsonString, null, + ignoreFromJson: true); + return response; + } catch (e) { + return ActionSingleDataResponse( + status: ResponseStatus.failed, errorMessage: e.toString()); + } + } + + Future broadcastTransactionUsingHiveSigner( String accessToken, BroadcastModel args) async { final url = Uri.parse('https://hivesigner.com/api/broadcast'); diff --git a/lib/core/services/data_service/mobile_service.dart b/lib/core/services/data_service/mobile_service.dart index 56a83ea..d6640da 100644 --- a/lib/core/services/data_service/mobile_service.dart +++ b/lib/core/services/data_service/mobile_service.dart @@ -96,12 +96,16 @@ Future voteContentFromPlatform( return response; } +//ADD custom json support for vote poll support here + Future getImageUploadProofWithPostingKeyFromPlatform( String username, String postingKey, ) async { - final String id = 'getImageUploadProofWithPostingKey${DateTime.now().toIso8601String()}'; - final String response = await platform.invokeMethod('getImageUploadProofWithPostingKey', { + final String id = + 'getImageUploadProofWithPostingKey${DateTime.now().toIso8601String()}'; + final String response = + await platform.invokeMethod('getImageUploadProofWithPostingKey', { 'id': id, 'username': username, 'postingKey': postingKey, @@ -128,4 +132,23 @@ Future muteUserFromPlatform( return response; } - +Future castPollVoteFromPlatform( + String username, + String pollId, + List choices, + String? postingKey, + String? authKey, + String? token, +) async { + final String id = 'castPollVote${DateTime.now().toIso8601String()}'; + final String response = await platform.invokeMethod('castPollVote', { + 'id': id, + 'username': username, + 'pollId': pollId, + 'choices': choices, + 'postingKey': postingKey ?? '', + 'token': token ?? '', + 'authKey': authKey ?? '' + }); + return response; +} diff --git a/lib/core/services/data_service/service.dart b/lib/core/services/data_service/service.dart index 5ffda69..3ac2612 100644 --- a/lib/core/services/data_service/service.dart +++ b/lib/core/services/data_service/service.dart @@ -43,6 +43,17 @@ Future voteContentFromPlatform( return _error(); } +Future castPollVoteFromPlatform( + String username, + String pollId, + List choices, + String? postingKey, + String? authKey, + String? token, +) async { + return _error(); +} + Future getImageUploadProofWithPostingKeyFromPlatform( String username, String postingKey, diff --git a/lib/core/services/data_service/web_service.dart b/lib/core/services/data_service/web_service.dart index a93e3b6..4c0ae52 100644 --- a/lib/core/services/data_service/web_service.dart +++ b/lib/core/services/data_service/web_service.dart @@ -79,6 +79,17 @@ Future voteContentFromPlatform( throw UnimplementedError(); } +Future castPollVoteFromPlatform( + String username, + String pollId, + List choices, + String? postingKey, + String? authKey, + String? token, +) async { + throw UnimplementedError(); +} + Future getImageUploadProofWithPostingKeyFromPlatform( String username, String postingKey, diff --git a/lib/features/threads/models/comment/comment_navigation_model.dart b/lib/features/threads/models/comment/comment_navigation_model.dart index 2867e09..2aec773 100644 --- a/lib/features/threads/models/comment/comment_navigation_model.dart +++ b/lib/features/threads/models/comment/comment_navigation_model.dart @@ -5,6 +5,8 @@ class SignTransactionNavigationModel { final String? permlink; final String? comment; final List? imageLinks; + final String? pollId; + final List? choices; final double? weight; final bool ishiveKeyChainMethod; final SignTransactionType transactionType; @@ -13,6 +15,8 @@ class SignTransactionNavigationModel { required this.author, this.permlink, this.imageLinks, + this.pollId, + this.choices, this.comment, this.weight, required this.transactionType, diff --git a/lib/features/threads/presentation/comments/add_comment/controller/sign_transaction_hive_controller.dart b/lib/features/threads/presentation/comments/add_comment/controller/sign_transaction_hive_controller.dart index a2e3761..9e5cafa 100644 --- a/lib/features/threads/presentation/comments/add_comment/controller/sign_transaction_hive_controller.dart +++ b/lib/features/threads/presentation/comments/add_comment/controller/sign_transaction_hive_controller.dart @@ -23,27 +23,35 @@ class SignTransactionHiveController extends HiveTransactionController { final double? weight; final SignTransactionType transactionType; String? _generatedPermlink; + final String? pollId; + final List? choices; - SignTransactionHiveController({ - required this.transactionType, - required this.author, - required this.authData, - required super.showError, - required super.onSuccess, - required super.onFailure, - required super.ishiveKeyChainMethod, - this.permlink, - this.comment, - this.imageLinks, - this.weight, - }) : assert( + SignTransactionHiveController( + {required this.transactionType, + required this.author, + required this.authData, + required super.showError, + required super.onSuccess, + required super.onFailure, + required super.ishiveKeyChainMethod, + this.permlink, + this.comment, + this.imageLinks, + this.weight, + this.pollId, + this.choices}) + : assert( !(transactionType == SignTransactionType.comment && (comment == null || imageLinks == null || permlink == null)), "comment,permlink and imageLinks parameters are required"), assert( !(transactionType == SignTransactionType.vote && (weight == null || permlink == null)), - "weight and permlink parameters are required") { + "weight and permlink parameters are required"), + assert( + !(transactionType == SignTransactionType.pollvote && + (weight == null || permlink == null)), + "pollId and choices parameters are required") { _initSignTransactionSocketSubscription(); } @@ -72,6 +80,7 @@ class SignTransactionHiveController extends HiveTransactionController { String get successMessage { switch (transactionType) { case SignTransactionType.vote: + case SignTransactionType.pollvote: return LocaleText.smVoteSuccessMessage; case SignTransactionType.comment: return LocaleText.smCommentPublishMessage; @@ -83,6 +92,7 @@ class SignTransactionHiveController extends HiveTransactionController { String get failureMessage { switch (transactionType) { case SignTransactionType.vote: + case SignTransactionType.pollvote: return LocaleText.emVoteFailureMessage; case SignTransactionType.comment: return LocaleText.emCommentDeclineMessage; @@ -134,6 +144,15 @@ class SignTransactionHiveController extends HiveTransactionController { authData.auth.authKey, authData.auth.token, ); + case SignTransactionType.pollvote: + return _threadRepository.castPollVote( + authData.accountName, + pollId!, + choices!, + null, + authData.auth.authKey, + authData.auth.token, + ); case SignTransactionType.mute: return _threadRepository.muteUser( authData.accountName, diff --git a/lib/features/threads/presentation/comments/add_comment/controller/sign_transaction_hive_signer_controller.dart b/lib/features/threads/presentation/comments/add_comment/controller/sign_transaction_hive_signer_controller.dart index 8227fec..b954cc9 100644 --- a/lib/features/threads/presentation/comments/add_comment/controller/sign_transaction_hive_signer_controller.dart +++ b/lib/features/threads/presentation/comments/add_comment/controller/sign_transaction_hive_signer_controller.dart @@ -71,6 +71,32 @@ class SignTransactionHiveSignerController { } } + Future initPollVoteProcess({ + required String pollId, + required List choices, + required UserAuthModel authdata, + required VoidCallback onSuccess, + required Function(String) showToast, + }) async { + ActionSingleDataResponse response = await _threadRepository + .broadcastTransactionUsingHiveSigner( + authdata.auth.token, + BroadcastModel( + type: BroadCastType.vote, + data: PollVoteBroadcastModel( + username: authdata.accountName, + pollId: pollId, + choices: choices, + )), + ); + if (response.isSuccess) { + showToast(LocaleText.smVoteSuccessMessage); + onSuccess(); + } else { + showToast(LocaleText.emVoteFailureMessage); + } + } + Future initMuteProcess({ required String author, required UserAuthModel authdata, diff --git a/lib/features/threads/presentation/comments/add_comment/controller/sign_transaction_posting_key_controller.dart b/lib/features/threads/presentation/comments/add_comment/controller/sign_transaction_posting_key_controller.dart index cf11fc6..f335d0d 100644 --- a/lib/features/threads/presentation/comments/add_comment/controller/sign_transaction_posting_key_controller.dart +++ b/lib/features/threads/presentation/comments/add_comment/controller/sign_transaction_posting_key_controller.dart @@ -60,6 +60,25 @@ class SignTransactionPostingKeyController { } } + Future initPollVoteProcess({ + required String pollId, + required List choices, + required UserAuthModel authdata, + required VoidCallback onSuccess, + required Function(String) showToast, + }) async { + + ActionSingleDataResponse pollVoteResponse = + await _threadRepository.castPollVote(authdata.accountName, pollId, + choices, authdata.auth.postingKey, null, null); + if (pollVoteResponse.isSuccess) { + showToast(LocaleText.smVoteSuccessMessage); + onSuccess(); + } else { + showToast(LocaleText.emVoteFailureMessage); + } + } + Future initMuteProcess({ required String author, required UserAuthModel authdata, diff --git a/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart b/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart index bd31e68..2b0f7c4 100644 --- a/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart +++ b/lib/features/threads/presentation/thread_feed/controller/poll_controller.dart @@ -1,8 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:waves/core/common/extensions/ui.dart'; import 'package:waves/core/models/action_response.dart'; +import 'package:waves/core/routes/routes.dart'; import 'package:waves/core/services/poll_service/poll_api.dart'; import 'package:waves/core/services/poll_service/poll_model.dart'; +import 'package:waves/core/utilities/enum.dart'; +import 'package:waves/features/auth/models/hive_signer_auth_model.dart'; +import 'package:waves/features/auth/models/posting_auth_model.dart'; import 'package:waves/features/auth/models/user_auth_model.dart'; +import 'package:waves/features/threads/models/comment/comment_navigation_model.dart'; +import 'package:waves/features/threads/presentation/comments/add_comment/controller/sign_transaction_hive_signer_controller.dart'; +import 'package:waves/features/threads/presentation/comments/add_comment/controller/sign_transaction_posting_key_controller.dart'; class PollController with ChangeNotifier { UserAuthModel? userData; @@ -44,7 +53,9 @@ class PollController with ChangeNotifier { } } - Future castVote( + + + Future castVote(BuildContext context, String author, String permlink, List selection) async { print("cast vote for $author $permlink using choice $selection"); @@ -59,14 +70,45 @@ class PollController with ChangeNotifier { return false; } - bool status = await Future.delayed(const Duration(seconds: 2), () => true); - - if (status) { - //TOOD: update poll model with update vote - poll.injectPollVoteCache(userData!.accountName, selection); - notifyListeners(); + // bool status = await Future.delayed(const Duration(seconds: 2), () => true); + if (userData!.isPostingKeyLogin) { + await SignTransactionPostingKeyController().initPollVoteProcess( + pollId:poll.pollTrxId, + choices: selection, + authdata: userData as UserAuthModel, + onSuccess: () { + poll.injectPollVoteCache(userData!.accountName, selection); + notifyListeners(); + }, + showToast: (message) => context.showSnackBar(message)); + } else if (userData!.isHiveSignerLogin) { + await SignTransactionHiveSignerController().initPollVoteProcess( + pollId:poll.pollTrxId, + choices: selection, + authdata: userData as UserAuthModel, + onSuccess: () { + poll.injectPollVoteCache(userData!.accountName, selection); + notifyListeners(); + }, + showToast: (message) => context.showSnackBar(message)); + } else { + SignTransactionNavigationModel navigationData = + SignTransactionNavigationModel( + transactionType: SignTransactionType.pollvote, + author: userData!.accountName, + pollId: poll.pollTrxId, + choices: selection, + ishiveKeyChainMethod: true); + context.pushNamed(Routes.hiveSignTransactionView, extra: navigationData) + .then((value) { + if (value != null) { + poll.injectPollVoteCache(userData!.accountName, selection); + notifyListeners(); + } + }); } - return status; + + return true; } List userVotedIds(String author, String permlink) => diff --git a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart index 2db5eec..e744531 100644 --- a/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart +++ b/lib/features/threads/presentation/thread_feed/widgets/post_poll/post_poll.dart @@ -70,16 +70,14 @@ class _PostPollState extends State { .map((entry) => entry.key) .toList(); - bool status = await pollController.castVote( - poll!.author, poll.permlink, selectedIds); - - if (status) { - setState(() { - enableRevote = false; - isVoting = false; - selection = {}; - }); - } + await pollController.castVote( + context, poll!.author, poll.permlink, selectedIds); + + setState(() { + enableRevote = false; + isVoting = false; + selection = {}; + }); } } diff --git a/lib/features/threads/repository/thread_repository.dart b/lib/features/threads/repository/thread_repository.dart index a6a5776..1ed2114 100644 --- a/lib/features/threads/repository/thread_repository.dart +++ b/lib/features/threads/repository/thread_repository.dart @@ -61,6 +61,21 @@ class ThreadRepository { username, author, permlink, weight, postingKey, authKey, token); } + + Future> castPollVote( + String username, + String pollId, + List choices, + String? postingKey, + String? authKey, + String? token, + ) async { + return await _apiService.castPollVote( + username, pollId, choices, postingKey, authKey, token); + } + + + Future broadcastTransactionUsingHiveSigner( String token, BroadcastModel data) async { return await _apiService.broadcastTransactionUsingHiveSigner(token, data); From 9260a5f0b5ac88db6369baa5085f0c5c9b010106 Mon Sep 17 00:00:00 2001 From: feruzm Date: Mon, 19 Aug 2024 09:12:37 +0530 Subject: [PATCH 18/18] version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 4e28805..ac6179a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+18 +version: 1.0.0+19 environment: sdk: '>=3.3.2 <4.0.0'