diff --git a/android/app/src/main/assets/index.html b/android/app/src/main/assets/index.html index 01946eb..cab950b 100644 --- a/android/app/src/main/assets/index.html +++ b/android/app/src/main/assets/index.html @@ -1,7 +1,8 @@ - - - - - - waves - - + + - - - - - - - - + + + + + + - - + } + + + + \ No newline at end of file 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 0ad71c2..9fd5dc5 100644 --- a/android/app/src/main/kotlin/com/ecency/waves/MainActivity.kt +++ b/android/app/src/main/kotlin/com/ecency/waves/MainActivity.kt @@ -103,6 +103,12 @@ class MainActivity: FlutterActivity() { "getImageUploadProofWithPostingKey('$id', '$username', '$postingKey');", null ) + } else if (call.method == "muteUser" && username != null && author != null + && postingKey != null && token != null && authKey != null ) { + webView?.evaluateJavascript( + "muteUser('$id','$username', '$author', '$postingKey', '$token', '$authKey');", + null + ) } } } diff --git a/ios/Runner/AppBridge.swift b/ios/Runner/AppBridge.swift index c8ab170..58e3eac 100644 --- a/ios/Runner/AppBridge.swift +++ b/ios/Runner/AppBridge.swift @@ -147,6 +147,21 @@ class AppBridge: NSObject { id: id, jsCode: "voteContent('\(id)','\(username)', '\(author)', '\(permlink)', '\(weight)', '\(postingKey)', '\(token)', '\(authKey)');" ) { text in result(text) } + case "muteUser": + guard + let username = arguments ["username"] as? String, + let author = arguments ["author"] as? String, + let postingKey = arguments ["postingKey"] as? String, + let token = arguments ["token"] as? String, + let authKey = arguments ["authKey"] as? String + else { + debugPrint("username, author, postingKey, token, authKey - are note set") + return result(FlutterMethodNotImplemented) + } + webVC.runThisJS( + id: id, + jsCode: "muteUser('\(id)','\(username)', '\(author)', '\(postingKey)', '\(token)', '\(authKey)');" + ) { text in result(text) } case "getImageUploadProofWithPostingKey": guard let username = arguments ["username"] as? String, diff --git a/ios/Runner/public/index.html b/ios/Runner/public/index.html index 130f6e9..33d2f5f 100644 --- a/ios/Runner/public/index.html +++ b/ios/Runner/public/index.html @@ -265,6 +265,37 @@ ); } + function muteUser( + id, + username, + author, + postingKey, + token, + authKey + ) { + let op = [ + "custom_json", + { + "id": "follow", + "required_auths": [], + "required_posting_auths": [ + username + ], + "json": JSON.stringify(["follow", { "follower": username, "following": author, "what": ["ignore"] }]) + } + + ]; + performOperations( + id, + [op], + "mute", + username, + postingKey, + token, + authKey + ); + } + function performOperations( id, operations, diff --git a/lib/core/common/widgets/dialog/dialog_template.dart b/lib/core/common/widgets/dialog/dialog_template.dart index 9090b19..e8563da 100644 --- a/lib/core/common/widgets/dialog/dialog_template.dart +++ b/lib/core/common/widgets/dialog/dialog_template.dart @@ -25,7 +25,7 @@ class DialogTemplate extends StatelessWidget { final theme = Theme.of(context); return AlertDialog( contentPadding: EdgeInsets.zero, - backgroundColor: theme.primaryColorLight, + backgroundColor: theme.colorScheme.tertiary, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(12))), title: Row( @@ -45,7 +45,10 @@ class DialogTemplate extends StatelessWidget { icon: const Icon(Icons.cancel)) ], ), - content: content, + content: Padding( + padding: const EdgeInsets.only(left: 24, right: 15, bottom: 20), + child: content, + ), actions: [ if (declineButtonText != null) DialogButton( diff --git a/lib/core/common/widgets/dialog/log_in_dialog.dart b/lib/core/common/widgets/dialog/log_in_dialog.dart index 8c99db5..7a5a00d 100644 --- a/lib/core/common/widgets/dialog/log_in_dialog.dart +++ b/lib/core/common/widgets/dialog/log_in_dialog.dart @@ -11,12 +11,7 @@ class LogInDialog extends StatelessWidget { Widget build(BuildContext context) { return DialogTemplate( title: LocaleText.notLoggedIn, - content: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - ).copyWith(bottom: 20), - child: Text(LocaleText.pleaseLoginFirst), - ), + content: Text(LocaleText.pleaseLoginFirst), declineButtonText: LocaleText.cancel, proceedButtonText: LocaleText.login, onProceedTap: () { diff --git a/lib/core/models/broadcast_model.dart b/lib/core/models/broadcast_model.dart index f15c047..bb543d6 100644 --- a/lib/core/models/broadcast_model.dart +++ b/lib/core/models/broadcast_model.dart @@ -6,6 +6,22 @@ class BroadcastModel { final T data; const BroadcastModel({required this.type, required this.data}); + + Map toJson() { + return _toJson(data); + } + + Map _toJson(T model) { + if (model is VoteBroadCastModel) { + return model.toJson(); + } else if (model is CommentBroadCastModel) { + return model.toJson(); + } else if (model is MuteBroadcastModel) { + return model.toJson(); + } else { + throw Exception('Unknown type'); + } + } } class VoteBroadCastModel { @@ -53,16 +69,35 @@ class CommentBroadCastModel { 'title': "", 'body': comment, 'json_metadata': json.encode({ - 'tags': [ - "hive-125125", - "waves", - "ecency", - "mobile", - "thread" - ], + 'tags': ["hive-125125", "waves", "ecency", "mobile", "thread"], 'app': "ecency-waves", 'format': "markdown+html", }), }; } } + +class MuteBroadcastModel { + final String username; + final String author; + + const MuteBroadcastModel({ + required this.username, + required this.author, + }); + + Map toJson() { + return { + "id": "follow", + "required_posting_auths": [username], + "json": json.encode([ + "follow", + { + "follower": username, + "following": author, + "what": ["ignore"] + } + ]) + }; + } +} diff --git a/lib/core/providers/global_providers.dart b/lib/core/providers/global_providers.dart index f71ff0b..eebf975 100644 --- a/lib/core/providers/global_providers.dart +++ b/lib/core/providers/global_providers.dart @@ -9,12 +9,20 @@ class GlobalProviders { ChangeNotifierProvider( create: (context) => ThemeController(), ), - ChangeNotifierProvider( + ChangeNotifierProvider( lazy: false, create: (context) => UserController(), ), - ChangeNotifierProvider( - create: (context) => ThreadFeedController(), + ChangeNotifierProxyProvider( + create: (context) => ThreadFeedController(observer: null), + update: (context, userController, previousThreadFeedController) { + if (previousThreadFeedController == null) { + return ThreadFeedController(observer: userController.userName); + } else { + previousThreadFeedController.updateObserver(userController.userName); + return previousThreadFeedController; + } + }, ) ]; } diff --git a/lib/core/services/data_service/api_service.dart b/lib/core/services/data_service/api_service.dart index 96ff70b..1938c76 100644 --- a/lib/core/services/data_service/api_service.dart +++ b/lib/core/services/data_service/api_service.dart @@ -16,16 +16,17 @@ import 'package:waves/core/utilities/enum.dart'; import 'package:waves/features/threads/models/comment/image_upload_error_response.dart'; import 'package:waves/features/threads/models/comment/image_upload_response.dart'; import 'package:waves/features/threads/models/post_detail/comment_model.dart'; +import 'package:waves/features/threads/models/thread_feeds/reported/report_reponse.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; import 'package:waves/features/user/models/follow_count_model.dart'; import 'package:waves/features/user/models/user_model.dart'; class ApiService { Future> getComments( - String accountName, String permlink) async { + String accountName, String permlink, String? observer) async { try { var url = Uri.parse( - 'https://hivexplorer.com/api/get_discussion?author=$accountName&permlink=$permlink'); + 'https://hivexplorer.com/api/get_discussion?author=$accountName&permlink=$permlink&observer=$observer'); var response = await http.get( url, @@ -151,7 +152,8 @@ class ApiService { String jsonString = await validatePostingKeyFromPlatform(accountName, postingKey); ActionSingleDataResponse response = - ActionSingleDataResponse.fromJsonString(jsonString, null,ignoreFromJson: true); + ActionSingleDataResponse.fromJsonString(jsonString, null, + ignoreFromJson: true); return response; } catch (e) { return ActionSingleDataResponse( @@ -214,12 +216,7 @@ class ApiService { }; final body = json.encode({ 'operations': [ - [ - enumToString(args.type), - (args.data is VoteBroadCastModel - ? (args.data as VoteBroadCastModel).toJson() - : (args.data as CommentBroadCastModel).toJson()) - ] + [enumToString(args.type), args.toJson()] ] }); @@ -374,4 +371,50 @@ class ApiService { status: ResponseStatus.failed, errorMessage: e.toString()); } } + + Future> muteUser( + String username, + String author, + String? postingKey, + String? authKey, + String? token, + ) async { + try { + String jsonString = await muteUserFromPlatform( + username, author, postingKey, authKey, token); + ActionSingleDataResponse response = + ActionSingleDataResponse.fromJsonString(jsonString, null, + ignoreFromJson: true); + return response; + } catch (e) { + return ActionSingleDataResponse( + status: ResponseStatus.failed, errorMessage: e.toString()); + } + } + + Future> reportThread( + String author, String permlink) async { + try { + var url = Uri.parse("https://ecency.com/private-api/report"); + + http.Response response = await http.post(url, + body: json.encode({ + "type": "content", + "data": "https://ecency.com/@$author/$permlink" + })); + if (response.statusCode == 200) { + return ActionSingleDataResponse( + data: ReportResponse.fromRawJson(response.body), + 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()); + } + } } diff --git a/lib/core/services/data_service/mobile_service.dart b/lib/core/services/data_service/mobile_service.dart index d070eae..56a83ea 100644 --- a/lib/core/services/data_service/mobile_service.dart +++ b/lib/core/services/data_service/mobile_service.dart @@ -109,4 +109,23 @@ Future getImageUploadProofWithPostingKeyFromPlatform( return response; } +Future muteUserFromPlatform( + String username, + String author, + String? postingKey, + String? authKey, + String? token, +) async { + final String id = 'muteUser${DateTime.now().toIso8601String()}'; + final String response = await platform.invokeMethod('muteUser', { + 'id': id, + 'username': username, + 'author': author, + '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 5f306c1..5ffda69 100644 --- a/lib/core/services/data_service/service.dart +++ b/lib/core/services/data_service/service.dart @@ -50,6 +50,16 @@ Future getImageUploadProofWithPostingKeyFromPlatform( return _error(); } +Future muteUserFromPlatform( + String username, + String author, + String? postingKey, + String? authKey, + String? token, +){ + return _error(); +} + Future _error() { return Future.value('error'); } diff --git a/lib/core/services/data_service/web_service.dart b/lib/core/services/data_service/web_service.dart index 4a910e6..a93e3b6 100644 --- a/lib/core/services/data_service/web_service.dart +++ b/lib/core/services/data_service/web_service.dart @@ -85,3 +85,13 @@ Future getImageUploadProofWithPostingKeyFromPlatform( ) { throw UnimplementedError(); } + +Future muteUserFromPlatform( + String username, + String author, + String? postingKey, + String? authKey, + String? token, +) { + throw UnimplementedError(); +} diff --git a/lib/core/services/local_service.dart b/lib/core/services/local_service.dart index 8407526..9679f5e 100644 --- a/lib/core/services/local_service.dart +++ b/lib/core/services/local_service.dart @@ -1,10 +1,12 @@ import 'package:get_storage/get_storage.dart'; import 'package:waves/core/utilities/enum.dart'; +import 'package:waves/features/threads/models/thread_feeds/reported/thread_info_model.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; class LocalService { final GetStorage _getStorage; final _defaultThreadKey = 'defaultThread'; + final _reportedThreadKey = 'reportedThreads'; LocalService({required GetStorage getStorage}) : _getStorage = getStorage; @@ -30,4 +32,17 @@ class LocalService { if (data == null) return null; return enumFromString(data, ThreadFeedType.values); } + + List readReportedThreads() { + String? data = _getStorage.read(_reportedThreadKey); + if (data == null) return []; + return ThreadInfoModel.fromRawJsonList(data); + } + + Future writeReportedThreads(ThreadInfoModel item) async { + List threads = readReportedThreads(); + threads.add(item); + await _getStorage.write( + _reportedThreadKey, ThreadInfoModel.toRawJsonList(threads)); + } } diff --git a/lib/core/utilities/enum.dart b/lib/core/utilities/enum.dart index e583296..e2c0ede 100644 --- a/lib/core/utilities/enum.dart +++ b/lib/core/utilities/enum.dart @@ -18,9 +18,9 @@ enum AuthType {hiveKeyChain, hiveAuth, postingKey, hiveSign} enum TransactionState {loading,qr,redirection} -enum SignTransactionType {comment,vote} +enum SignTransactionType {comment,vote,mute} -enum BroadCastType {vote,comment} +enum BroadCastType {vote,comment,custom_json} enum FollowType { followers, following } diff --git a/lib/core/utilities/generics/classes/thread.dart b/lib/core/utilities/generics/classes/thread.dart index 7540927..5214fb1 100644 --- a/lib/core/utilities/generics/classes/thread.dart +++ b/lib/core/utilities/generics/classes/thread.dart @@ -1,8 +1,8 @@ import 'package:waves/core/utilities/enum.dart'; +import 'package:waves/features/threads/models/thread_feeds/reported/thread_info_model.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; class Thread { - static ThreadFeedType defaultThreadType = ThreadFeedType.ecency; static void sortList(List list, {bool isAscending = false}) { @@ -30,6 +30,20 @@ class Thread { return result; } + static List filterReportedThreads({ + required List items, + required List reportedThreads, + }) { + List result = items.where((element) { + return !reportedThreads.any((reported) => + reported.author == element.author && + reported.permlink == element.permlink); + }).toList(); + + sortList(result, isAscending: false); + return result; + } + static String getThreadImage({required ThreadFeedType type}) { switch (type) { case ThreadFeedType.ecency: diff --git a/lib/features/threads/models/comment/comment_navigation_model.dart b/lib/features/threads/models/comment/comment_navigation_model.dart index 883f9c6..2867e09 100644 --- a/lib/features/threads/models/comment/comment_navigation_model.dart +++ b/lib/features/threads/models/comment/comment_navigation_model.dart @@ -2,7 +2,7 @@ import 'package:waves/core/utilities/enum.dart'; class SignTransactionNavigationModel { final String author; - final String permlink; + final String? permlink; final String? comment; final List? imageLinks; final double? weight; @@ -11,7 +11,7 @@ class SignTransactionNavigationModel { SignTransactionNavigationModel({ required this.author, - required this.permlink, + this.permlink, this.imageLinks, this.comment, this.weight, diff --git a/lib/features/threads/models/thread_feeds/reported/report_reponse.dart b/lib/features/threads/models/thread_feeds/reported/report_reponse.dart new file mode 100644 index 0000000..923aa8b --- /dev/null +++ b/lib/features/threads/models/thread_feeds/reported/report_reponse.dart @@ -0,0 +1,64 @@ +import 'dart:convert'; + +class ReportResponse { + final int statusCode; + final ReportStatus status; + final bool isSuccess; + + ReportResponse({ + required this.statusCode, + required this.status, + }): isSuccess = statusCode == 200 && status.value.toLowerCase() == "ok"; + + ReportResponse copyWith({ + int? statusCode, + ReportStatus? status, + }) => + ReportResponse( + statusCode: statusCode ?? this.statusCode, + status: status ?? this.status, + ); + + factory ReportResponse.fromRawJson(String str) => + ReportResponse.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory ReportResponse.fromJson(Map json) => ReportResponse( + statusCode: json["status"], + status: ReportStatus.fromJson(json["body"]), + ); + + Map toJson() => { + "status": statusCode, + "body": status.toJson(), + }; +} + +class ReportStatus { + final String value; + + ReportStatus({ + required this.value, + }); + + ReportStatus copyWith({ + String? value, + }) => + ReportStatus( + value: value ?? this.value, + ); + + factory ReportStatus.fromRawJson(String str) => + ReportStatus.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory ReportStatus.fromJson(Map json) => ReportStatus( + value: json["status"], + ); + + Map toJson() => { + "status": value, + }; +} diff --git a/lib/features/threads/models/thread_feeds/reported/thread_info_model.dart b/lib/features/threads/models/thread_feeds/reported/thread_info_model.dart new file mode 100644 index 0000000..354bc76 --- /dev/null +++ b/lib/features/threads/models/thread_feeds/reported/thread_info_model.dart @@ -0,0 +1,24 @@ +import 'dart:convert'; + +import 'package:waves/features/threads/presentation/thread_feed/view_models/view_model.dart'; + +class ThreadInfoModel extends ThreadInfo { + const ThreadInfoModel({required super.author, required super.permlink}); + + factory ThreadInfoModel.fromJson(Map json) => + ThreadInfoModel(author: json['author'], permlink: json['permlink']); + + Map toJson() => { + 'author': author, + 'permlink': permlink, + }; + + static List fromRawJsonList(String string) { + List data = json.decode(string); + return data.map((e) => ThreadInfoModel.fromJson(e)).toList(); + } + + static String toRawJsonList(List threads) { + return json.encode(threads.map((e) => e.toJson()).toList()); + } +} 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 a37e129..a2e3761 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 @@ -16,7 +16,7 @@ class SignTransactionHiveController extends HiveTransactionController { final ThreadRepository _threadRepository = getIt(); late final StreamSubscription _socketSubscription; final String author; - final String permlink; + final String? permlink; final UserAuthModel authData; final String? comment; final List? imageLinks; @@ -27,21 +27,23 @@ class SignTransactionHiveController extends HiveTransactionController { SignTransactionHiveController({ required this.transactionType, required this.author, - required this.permlink, required this.authData, required super.showError, required super.onSuccess, required super.onFailure, required super.ishiveKeyChainMethod, + this.permlink, this.comment, this.imageLinks, this.weight, }) : assert( !(transactionType == SignTransactionType.comment && - (comment == null || imageLinks == null)), - "comment and imageLinks parameters are required"), - assert(!(transactionType == SignTransactionType.vote && weight == null), - "weight parameter is required") { + (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") { _initSignTransactionSocketSubscription(); } @@ -68,18 +70,24 @@ class SignTransactionHiveController extends HiveTransactionController { } String get successMessage { - if (transactionType == SignTransactionType.vote) { - return LocaleText.smVoteSuccessMessage; - } else { - return LocaleText.smCommentPublishMessage; + switch (transactionType) { + case SignTransactionType.vote: + return LocaleText.smVoteSuccessMessage; + case SignTransactionType.comment: + return LocaleText.smCommentPublishMessage; + case SignTransactionType.mute: + return "User is muted successfully"; } } String get failureMessage { - if (transactionType == SignTransactionType.vote) { - return LocaleText.emVoteFailureMessage; - } else { - return LocaleText.emCommentDeclineMessage; + switch (transactionType) { + case SignTransactionType.vote: + return LocaleText.emVoteFailureMessage; + case SignTransactionType.comment: + return LocaleText.emCommentDeclineMessage; + case SignTransactionType.mute: + return "Mute operation is failed"; } } @@ -110,7 +118,7 @@ class SignTransactionHiveController extends HiveTransactionController { return _threadRepository.commentOnContent( authData.accountName, author, - permlink, //parentPermlink + permlink!, //parentPermlink _generatedPermlink!, Act.commentWithImages(comment!, imageLinks!), null, @@ -120,12 +128,20 @@ class SignTransactionHiveController extends HiveTransactionController { return _threadRepository.votecontent( authData.accountName, author, - permlink, + permlink!, weight!, null, authData.auth.authKey, authData.auth.token, ); + case SignTransactionType.mute: + return _threadRepository.muteUser( + authData.accountName, + author, + null, + authData.auth.authKey, + authData.auth.token, + ); } } 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 f70ce7d..8227fec 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 @@ -12,20 +12,18 @@ import 'package:waves/features/threads/repository/thread_repository.dart'; class SignTransactionHiveSignerController { final ThreadRepository _threadRepository = getIt(); - Future initCommentProcess( - String comment, { - required String parentAuthor, - required String parentPermlink, - required UserAuthModel authData, - required Function(String) onSuccess, - required VoidCallback onFailure, - required Function(String) showToast, - required List imageLinks - }) async { + Future initCommentProcess(String comment, + {required String parentAuthor, + required String parentPermlink, + required UserAuthModel authData, + required Function(String) onSuccess, + required VoidCallback onFailure, + required Function(String) showToast, + required List imageLinks}) async { String generatedPermlink = Act.generatePermlink(authData.accountName); String commentWithImages = Act.commentWithImages(comment, imageLinks); - ActionSingleDataResponse commentResponse = - await _threadRepository.commentUsingHiveSigner( + ActionSingleDataResponse commentResponse = await _threadRepository + .broadcastTransactionUsingHiveSigner( authData.auth.token, BroadcastModel( type: BroadCastType.comment, @@ -54,8 +52,8 @@ class SignTransactionHiveSignerController { required VoidCallback onSuccess, required Function(String) showToast, }) async { - ActionSingleDataResponse response = - await _threadRepository.voteUsingHiveSigner( + ActionSingleDataResponse response = await _threadRepository + .broadcastTransactionUsingHiveSigner( authdata.auth.token, BroadcastModel( type: BroadCastType.vote, @@ -72,4 +70,31 @@ class SignTransactionHiveSignerController { showToast(LocaleText.emVoteFailureMessage); } } + + Future initMuteProcess({ + required String author, + required UserAuthModel authdata, + required VoidCallback onSuccess, + required VoidCallback onFailure, + required Function(String) showToast, + }) async { + ActionSingleDataResponse response = await _threadRepository + .broadcastTransactionUsingHiveSigner( + authdata.auth.token, + BroadcastModel( + type: BroadCastType.custom_json, + data: MuteBroadcastModel( + username: authdata.accountName, + author: author, + ), + ), + ); + if (response.isSuccess) { + showToast("User is muted successfully"); + onSuccess(); + } else { + showToast("Mute operation is failed"); + onFailure(); + } + } } 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 6ad123b..cf11fc6 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 @@ -59,4 +59,23 @@ class SignTransactionPostingKeyController { showToast(LocaleText.emVoteFailureMessage); } } + + Future initMuteProcess({ + required String author, + required UserAuthModel authdata, + required VoidCallback onSuccess, + required VoidCallback onFailure, + required Function(String) showToast, + }) async { + ActionSingleDataResponse response = + await _threadRepository.muteUser( + authdata.accountName, author, authdata.auth.postingKey, null, null); + if (response.isSuccess) { + showToast("User is muted successfully"); + onSuccess(); + } else { + showToast("Mute operation is failed"); + onFailure(); + } + } } diff --git a/lib/features/threads/presentation/comments/add_comment/view/add_comment_view.dart b/lib/features/threads/presentation/comments/add_comment/view/add_comment_view.dart index db3331b..692d3fe 100644 --- a/lib/features/threads/presentation/comments/add_comment/view/add_comment_view.dart +++ b/lib/features/threads/presentation/comments/add_comment/view/add_comment_view.dart @@ -22,7 +22,8 @@ class AddCommentView extends StatefulWidget { } class _AddCommentViewState extends State { - final TextEditingController commentTextEditingController = TextEditingController(); + final TextEditingController commentTextEditingController = + TextEditingController(); late final bool isRoot; final FocusNode _nodeText = FocusNode(); @@ -37,7 +38,7 @@ class _AddCommentViewState extends State { (node) { return GestureDetector( onTap: () => node.unfocus(), - child: Padding( + child: const Padding( padding: EdgeInsets.all(8.0), child: Text( "Done", @@ -79,7 +80,8 @@ class _AddCommentViewState extends State { minLines: 1, focusNode: _nodeText, textInputAction: TextInputAction.newline, - decoration: InputDecoration(hintText: hintText, border: InputBorder.none), + decoration: + InputDecoration(hintText: hintText, border: InputBorder.none), ), ), ), diff --git a/lib/features/threads/presentation/comments/comment_detail/controller/comment_detail_controller.dart b/lib/features/threads/presentation/comments/comment_detail/controller/comment_detail_controller.dart index c0d476a..bd272a7 100644 --- a/lib/features/threads/presentation/comments/comment_detail/controller/comment_detail_controller.dart +++ b/lib/features/threads/presentation/comments/comment_detail/controller/comment_detail_controller.dart @@ -11,21 +11,21 @@ class CommentDetailController extends ChangeNotifier { final String author; final String permlink; + final String? observer; ThreadFeedModel mainThread; List items = []; ViewState viewState = ViewState.loading; - CommentDetailController({ - required this.mainThread, - }) : author = mainThread.author, + CommentDetailController({required this.mainThread, required this.observer}) + : author = mainThread.author, permlink = mainThread.permlink { _init(); } void _init() async { ActionListDataResponse response = - await _repository.getcomments(author, permlink); + await _repository.getcomments(author, permlink, observer); if (response.isSuccess) { if (response.data!.isNotEmpty) { List? thread = response.data! @@ -36,7 +36,8 @@ class CommentDetailController extends ChangeNotifier { response.data!.remove(thread.first); } items = response.data!; - items = Thread.filterTopLevelComments(permlink,items: items,depth: mainThread.depth+1 ); + items = Thread.filterTopLevelComments(permlink, + items: items, depth: mainThread.depth + 1); if (items.isNotEmpty) { viewState = ViewState.data; } else { @@ -59,7 +60,7 @@ class CommentDetailController extends ChangeNotifier { void onCommentAdded(ThreadFeedModel thread) { mainThread = mainThread.copyWith(children: mainThread.children! + 1); - items = [thread,...items]; + items = [thread, ...items]; if (viewState == ViewState.empty) { viewState = ViewState.data; } diff --git a/lib/features/threads/presentation/comments/comment_detail/view/comment_detail_view.dart b/lib/features/threads/presentation/comments/comment_detail/view/comment_detail_view.dart index 91428b7..754e469 100644 --- a/lib/features/threads/presentation/comments/comment_detail/view/comment_detail_view.dart +++ b/lib/features/threads/presentation/comments/comment_detail/view/comment_detail_view.dart @@ -32,7 +32,8 @@ class CommentDetailView extends StatelessWidget { final theme = Theme.of(context); final userController = context.read(); return ChangeNotifierProvider( - create: (context) => CommentDetailController(mainThread: item), + create: (context) => CommentDetailController( + mainThread: item, observer: userController.userName), builder: (context, child) { return Selector( selector: (_, myType) => myType.mainThread, diff --git a/lib/features/threads/presentation/thread_feed/controller/thread_feed_controller.dart b/lib/features/threads/presentation/thread_feed/controller/thread_feed_controller.dart index 7b099b2..88fbd4c 100644 --- a/lib/features/threads/presentation/thread_feed/controller/thread_feed_controller.dart +++ b/lib/features/threads/presentation/thread_feed/controller/thread_feed_controller.dart @@ -7,6 +7,8 @@ import 'package:waves/core/utilities/generics/classes/thread.dart'; import 'package:waves/core/utilities/generics/controllers/controller_interface.dart'; import 'package:waves/core/utilities/generics/mixins/pagination_mixin.dart'; import 'package:waves/features/threads/models/post_detail/upvote_model.dart'; +import 'package:waves/features/threads/models/thread_feeds/reported/report_reponse.dart'; +import 'package:waves/features/threads/models/thread_feeds/reported/thread_info_model.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_bookmark_model.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; import 'package:waves/features/threads/presentation/thread_feed/view_models/view_model.dart'; @@ -22,6 +24,7 @@ class ThreadFeedController extends ChangeNotifier final AccountPostType _postType = AccountPostType.posts; final BookmarkProvider bookmarkProvider = BookmarkProvider(type: BookmarkType.thread); + String? observer; int currentPage = 0; bool isDataDisplayedFromServer = false; List pages = []; @@ -33,7 +36,7 @@ class ThreadFeedController extends ChangeNotifier @override ViewState viewState = ViewState.loading; - ThreadFeedController() { + ThreadFeedController({required this.observer}) { threadType = _localRepository.readDefaultThread(); super.pageLimit = 10; init(); @@ -48,6 +51,11 @@ class ThreadFeedController extends ChangeNotifier } } + Future updateObserver(String? newObserver) async { + observer = newObserver; + await refresh(); + } + Future _loadSingleFeedType(ThreadFeedType type) async { _loadLocalThreads(type); ActionListDataResponse accountPostResponse = @@ -63,13 +71,18 @@ class ThreadFeedController extends ChangeNotifier ActionListDataResponse response = await _repository.getcomments( accountPostResponse.data!.first.author, - accountPostResponse.data!.first.permlink); + accountPostResponse.data!.first.permlink, + observer); if (response.isSuccess && response.data != null) { if (type == threadType) { List items = response.data!; items = filterTopLevelComments( accountPostResponse.data!.first.permlink, items: items); + items = Thread.filterReportedThreads( + items: items, + reportedThreads: _localRepository.readReportedThreads()); + if (items.isEmpty && viewState != ViewState.data) { viewState = ViewState.empty; } else if (items.isNotEmpty) { @@ -141,8 +154,9 @@ class ThreadFeedController extends ChangeNotifier ActionSingleDataResponse postResponse = await _repository .getFirstAccountPost(_getThreadAccountName(type: type), _postType, 1); if (postResponse.isSuccess) { - ActionListDataResponse response = await _repository - .getcomments(postResponse.data!.author, postResponse.data!.permlink); + ActionListDataResponse response = + await _repository.getcomments( + postResponse.data!.author, postResponse.data!.permlink, observer); if (response.isSuccess) { return filterTopLevelComments(postResponse.data!.permlink, items: response.data); @@ -219,8 +233,8 @@ class ThreadFeedController extends ChangeNotifier if (!super.isPageEnded && currentPage < pages.length) { notifyListeners(); ActionListDataResponse response = - await _repository.getcomments( - pages[currentPage].author, pages[currentPage].permlink); + await _repository.getcomments(pages[currentPage].author, + pages[currentPage].permlink, observer); if (response.isSuccess && response.data != null && type == threadType) { List newItems = filterTopLevelComments( pages[currentPage].permlink, @@ -240,6 +254,19 @@ class ThreadFeedController extends ChangeNotifier } } + Future reportThread(String author, String permlink) async { + ActionSingleDataResponse response = + await _repository.reportThread(author, permlink); + if (response.isSuccess && response.data!.isSuccess) { + _localRepository.writeReportedThreads( + ThreadInfoModel(author: author, permlink: permlink)); + refresh(); + return true; + } else { + return false; + } + } + @override Future refresh() async { if (viewState != ViewState.data) { diff --git a/lib/features/threads/presentation/thread_feed/view_models/view_model.dart b/lib/features/threads/presentation/thread_feed/view_models/view_model.dart index 4ce38a3..c171e5b 100644 --- a/lib/features/threads/presentation/thread_feed/view_models/view_model.dart +++ b/lib/features/threads/presentation/thread_feed/view_models/view_model.dart @@ -1,6 +1,11 @@ -class ThreadInfo { +import 'package:equatable/equatable.dart'; + +class ThreadInfo extends Equatable { final String author; final String permlink; - ThreadInfo({required this.author, required this.permlink}); + const ThreadInfo({required this.author, required this.permlink}); + + @override + List get props => [author,permlink]; } diff --git a/lib/features/threads/presentation/thread_feed/widgets/thread_pop_up_menu.dart b/lib/features/threads/presentation/thread_feed/widgets/thread_pop_up_menu.dart new file mode 100644 index 0000000..1508725 --- /dev/null +++ b/lib/features/threads/presentation/thread_feed/widgets/thread_pop_up_menu.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:waves/core/common/extensions/ui.dart'; +import 'package:waves/core/common/widgets/dialog/dialog_template.dart'; +import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; +import 'package:waves/features/threads/presentation/thread_feed/controller/thread_feed_controller.dart'; + +class ThreadPopUpMenu extends StatelessWidget { + const ThreadPopUpMenu({super.key, required this.item}); + + final ThreadFeedModel item; + + @override + Widget build(BuildContext context) { + final controller = context.read(); + return PopupMenuButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: 'option1', + child: Text( + 'Report', + style: TextStyle(color: Colors.red), + ), + ), + ], + onSelected: (String value) { + switch (value) { + case 'option1': + showDialog( + context: context, + builder: (_) { + return DialogTemplate( + title: "Report this", + content: const Text( + "Are you sure you want to report this user?", + ), + declineButtonText: "Cancel", + proceedButtonText: "Report", + onProceedTap: () async { + context.showLoader(); + controller + .reportThread(item.author, item.permlink) + .then((isSuccess) { + context.hideLoader(); + if (isSuccess) { + context.showSnackBar( + "User has been reported successfully"); + } else { + context.showSnackBar("Report failed"); + } + }); + }, + ); + }); + break; + } + }, + child: const Icon(Icons.more_vert), + ); + } +} diff --git a/lib/features/threads/presentation/thread_feed/widgets/thread_user_info_tile.dart b/lib/features/threads/presentation/thread_feed/widgets/thread_user_info_tile.dart index be6af42..8a9a1d1 100644 --- a/lib/features/threads/presentation/thread_feed/widgets/thread_user_info_tile.dart +++ b/lib/features/threads/presentation/thread_feed/widgets/thread_user_info_tile.dart @@ -6,6 +6,7 @@ import 'package:waves/core/common/widgets/images/user_profile_image.dart'; import 'package:waves/core/routes/route_keys.dart'; import 'package:waves/core/routes/routes.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; +import 'package:waves/features/threads/presentation/thread_feed/widgets/thread_pop_up_menu.dart'; class ThreadUserInfoTile extends StatelessWidget { const ThreadUserInfoTile({ @@ -34,26 +35,37 @@ class ThreadUserInfoTile extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ Expanded( - child: GestureDetector( - onTap: () => _pushToUserProfile(context), - child: Text( - item.author, - style: theme.textTheme.bodyMedium! - .copyWith(fontWeight: FontWeight.bold), + child: Row( + children: [ + Flexible( + child: GestureDetector( + onTap: () => _pushToUserProfile(context), + child: Text( + item.author, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodyMedium! + .copyWith(fontWeight: FontWeight.bold), + ), + ), ), - ), - ), + const Gap(10), + Text( + timeInString, + textAlign: TextAlign.end, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: theme.textTheme.labelLarge!.copyWith( + color: theme.primaryColorDark.withOpacity(0.8)), + ), + ], + )), const Gap( 12, ), - Text( - timeInString, - textAlign: TextAlign.end, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: theme.textTheme.labelLarge! - .copyWith(color: theme.primaryColorDark.withOpacity(0.8)), - ), + ThreadPopUpMenu( + item: item, + ) ], ), ) diff --git a/lib/features/threads/repository/thread_local_repository.dart b/lib/features/threads/repository/thread_local_repository.dart index 60f46a4..2259527 100644 --- a/lib/features/threads/repository/thread_local_repository.dart +++ b/lib/features/threads/repository/thread_local_repository.dart @@ -1,20 +1,29 @@ import 'package:waves/core/services/local_service.dart'; import 'package:waves/core/utilities/enum.dart'; import 'package:waves/features/settings/repository/settings_repository.dart'; +import 'package:waves/features/threads/models/thread_feeds/reported/thread_info_model.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; -class ThreadLocalRepository extends SettingsRepository{ +class ThreadLocalRepository extends SettingsRepository { final LocalService _localService; ThreadLocalRepository({required super.localService}) : _localService = localService; - List? readLocalThreads(ThreadFeedType type){ - return _localService.readThreads(type); + List? readLocalThreads(ThreadFeedType type) { + return _localService.readThreads(type); } Future writeLocalThreads( List threads, ThreadFeedType type) async { return await _localService.writeThreads(threads, type); } + + List readReportedThreads() { + return _localService.readReportedThreads(); + } + + Future writeReportedThreads(ThreadInfoModel item) async { + return await _localService.writeReportedThreads(item); + } } diff --git a/lib/features/threads/repository/thread_repository.dart b/lib/features/threads/repository/thread_repository.dart index 6403ac7..a6a5776 100644 --- a/lib/features/threads/repository/thread_repository.dart +++ b/lib/features/threads/repository/thread_repository.dart @@ -2,6 +2,7 @@ import 'package:waves/core/models/action_response.dart'; import 'package:waves/core/models/broadcast_model.dart'; import 'package:waves/core/services/data_service/api_service.dart'; import 'package:waves/core/utilities/enum.dart'; +import 'package:waves/features/threads/models/thread_feeds/reported/report_reponse.dart'; import 'package:waves/features/threads/models/thread_feeds/thread_feed_model.dart'; class ThreadRepository { @@ -29,8 +30,8 @@ class ThreadRepository { } Future> getcomments( - String accountName, String permlink) async { - return await _apiService.getComments(accountName, permlink); + String accountName, String permlink, String? observer) async { + return await _apiService.getComments(accountName, permlink, observer); } Future> commentOnContent( @@ -60,13 +61,24 @@ class ThreadRepository { username, author, permlink, weight, postingKey, authKey, token); } - Future voteUsingHiveSigner( - String token, BroadcastModel data) async { + Future broadcastTransactionUsingHiveSigner( + String token, BroadcastModel data) async { return await _apiService.broadcastTransactionUsingHiveSigner(token, data); } - Future commentUsingHiveSigner( - String token, BroadcastModel data) async { - return await _apiService.broadcastTransactionUsingHiveSigner(token, data); + Future> muteUser( + String username, + String author, + String? postingKey, + String? authKey, + String? token, + ) async { + return await _apiService.muteUser( + username, author, postingKey, authKey, token); + } + + Future> reportThread( + String author, String permlink) async { + return await _apiService.reportThread(author, permlink); } } diff --git a/lib/features/user/presentation/user_profile/widgets/user_profile_follow_mute_buttons.dart b/lib/features/user/presentation/user_profile/widgets/user_profile_follow_mute_buttons.dart index 97a1787..26954d4 100644 --- a/lib/features/user/presentation/user_profile/widgets/user_profile_follow_mute_buttons.dart +++ b/lib/features/user/presentation/user_profile/widgets/user_profile_follow_mute_buttons.dart @@ -1,17 +1,112 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:waves/core/common/extensions/ui.dart'; import 'package:waves/core/common/widgets/buttons/duo_text_buttons.dart'; +import 'package:waves/core/routes/routes.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'; +import 'package:waves/features/threads/presentation/thread_feed/controller/thread_feed_controller.dart'; +import 'package:waves/features/user/view/user_controller.dart'; -class UserProfileFollowMuteButtons extends StatelessWidget { - const UserProfileFollowMuteButtons({super.key, this.buttonHeight}); +class UserProfileFollowMuteButtons extends StatefulWidget { + const UserProfileFollowMuteButtons({ + super.key, + this.buttonHeight, + required this.author, + }); final double? buttonHeight; + final String author; + + @override + State createState() => + _UserProfileFollowMuteButtonsState(); +} + +class _UserProfileFollowMuteButtonsState + extends State { + late ThreadFeedController feedController; + + @override + void didChangeDependencies() { + feedController = context.read(); + super.didChangeDependencies(); + } @override Widget build(BuildContext context) { return DuoTextButtons( - buttonHeight: buttonHeight, - buttonOneText: "Follow", - buttonOneOnTap: () {}, + buttonHeight: widget.buttonHeight, + buttonOneText: "Mute", + buttonOneOnTap: () { + context.authenticatedAction(action: () { + final UserAuthModel userData = + context.read().userData!; + if (userData.isPostingKeyLogin) { + _postingKeyMuteTransaction(userData, context); + } else if (userData.isHiveSignerLogin) { + _hiveSignerMuteTransaction(userData, context); + } else { + _onTransactionDecision(AuthType.hiveKeyChain, context, userData); + } + }); + }, ); } + + void _postingKeyMuteTransaction( + UserAuthModel userData, BuildContext context) async { + context.showLoader(); + await SignTransactionPostingKeyController().initMuteProcess( + author: widget.author, + authdata: userData as UserAuthModel, + onSuccess: () { + context.hideLoader(); + refreshFeeds(); + }, + onFailure: () => context.hideLoader(), + showToast: (message) => context.showSnackBar(message)); + } + + void _hiveSignerMuteTransaction( + UserAuthModel userData, BuildContext context) async { + context.showLoader(); + await SignTransactionHiveSignerController().initMuteProcess( + author: widget.author, + authdata: userData as UserAuthModel, + onSuccess: () { + context.hideLoader(); + refreshFeeds(); + }, + onFailure: () => context.hideLoader(), + showToast: (message) => context.showSnackBar(message)); + } + + void _onTransactionDecision( + AuthType authType, BuildContext context, UserAuthModel userData) async { + SignTransactionNavigationModel navigationData = + SignTransactionNavigationModel( + transactionType: SignTransactionType.mute, + author: widget.author, + ishiveKeyChainMethod: authType == AuthType.hiveKeyChain); + context + .pushNamed(Routes.hiveSignTransactionView, extra: navigationData) + .then((value) { + if (value != null) { + refreshFeeds(); + } + }); + } + + void refreshFeeds() { + Future.delayed(const Duration(seconds: 3)).then((_) { + feedController.refresh(); + }); + } } diff --git a/lib/features/user/presentation/user_profile/widgets/user_profile_widget.dart b/lib/features/user/presentation/user_profile/widgets/user_profile_widget.dart index c78a534..6ac9b4c 100644 --- a/lib/features/user/presentation/user_profile/widgets/user_profile_widget.dart +++ b/lib/features/user/presentation/user_profile/widgets/user_profile_widget.dart @@ -73,10 +73,11 @@ class _UserProfileViewWidgetState extends State { ImageContainer( width: double.infinity, url: widget.data.postingJsonMetadata?.profile?.coverImage), - const Positioned( + Positioned( bottom: 10, right: 10, child: UserProfileFollowMuteButtons( + author: widget.data.name, buttonHeight: 30, ), ),