diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6b0eb150..e264e5e4 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -436,7 +436,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = G8RXR25D89; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -445,7 +445,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.33; + MARKETING_VERSION = 1.1.34; PRODUCT_BUNDLE_IDENTIFIER = com.agoradesk.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -526,7 +526,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = G8RXR25D89; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -535,7 +535,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.33; + MARKETING_VERSION = 1.1.34; PRODUCT_BUNDLE_IDENTIFIER = com.agoradesk.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -615,7 +615,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = G8RXR25D89; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -624,7 +624,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.33; + MARKETING_VERSION = 1.1.34; PRODUCT_BUNDLE_IDENTIFIER = co.localmonero.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -707,7 +707,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = G8RXR25D89; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -716,7 +716,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.33; + MARKETING_VERSION = 1.1.34; PRODUCT_BUNDLE_IDENTIFIER = co.localmonero.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -795,7 +795,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = G8RXR25D89; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -804,7 +804,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.33; + MARKETING_VERSION = 1.1.34; PRODUCT_BUNDLE_IDENTIFIER = com.agoradesk.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -882,7 +882,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = G8RXR25D89; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -891,7 +891,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.33; + MARKETING_VERSION = 1.1.34; PRODUCT_BUNDLE_IDENTIFIER = co.localmonero.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/lib/core/services/notifications/local_notifications_controller.dart b/lib/core/services/notifications/local_notifications_controller.dart index 64af376b..22e00635 100644 --- a/lib/core/services/notifications/local_notifications_controller.dart +++ b/lib/core/services/notifications/local_notifications_controller.dart @@ -61,6 +61,7 @@ class LocalNotificationController with ForegroundMessagesMixin { channelDescription: channel.description, icon: kNotificationIcon, color: const Color.fromRGBO(0, 0, 0, 1), + largeIcon: const DrawableResourceAndroidBitmap('@mipmap/ic_launcher'), // colorized: true, ), ), diff --git a/lib/features/account/models/trader_profile_view_model.dart b/lib/features/account/models/trader_profile_view_model.dart index 38d56646..91bf109e 100644 --- a/lib/features/account/models/trader_profile_view_model.dart +++ b/lib/features/account/models/trader_profile_view_model.dart @@ -6,9 +6,9 @@ import 'package:agoradesk/features/account/data/models/feedback_model.dart'; import 'package:agoradesk/features/account/data/models/feedback_type.dart'; import 'package:agoradesk/features/account/data/services/account_service.dart'; import 'package:agoradesk/features/ads/data/models/ad_model.dart'; +import 'package:agoradesk/features/ads/data/models/ads_request_parameter_model.dart'; import 'package:agoradesk/features/ads/data/repositories/ads_repository.dart'; import 'package:agoradesk/features/trades/models/note_on_user_view_model.dart'; -import 'package:provider/provider.dart'; import 'package:vm/vm.dart'; class TraderProfileViewModel extends ViewModel with ErrorParseMixin { @@ -87,7 +87,10 @@ class TraderProfileViewModel extends ViewModel with ErrorParseMixin { Future _getUserAds() async { loadingAds = true; ads.clear(); - final res = await _adsRepository.getUserAds(profileModel?.username ?? username!); + final res = await _adsRepository.getUserAds( + profileModel?.username ?? username!, + const AdsRequestParameterModel(), + ); loadingAds = false; if (res.isRight) { paginationMeta = res.right.pagination; diff --git a/lib/features/account/models/user_ads_view_model.dart b/lib/features/account/models/user_ads_view_model.dart index ab26c137..3eedf7a7 100644 --- a/lib/features/account/models/user_ads_view_model.dart +++ b/lib/features/account/models/user_ads_view_model.dart @@ -1,13 +1,21 @@ +import 'package:agoradesk/core/app_parameters.dart'; import 'package:agoradesk/core/extensions/capitalized_first_letter.dart'; import 'package:agoradesk/core/models/pagination.dart'; -import 'package:vm/vm.dart'; +import 'package:agoradesk/core/theme/theme.dart'; import 'package:agoradesk/core/translations/country_info_mixin.dart'; import 'package:agoradesk/core/translations/payment_method_mixin.dart'; import 'package:agoradesk/core/utils/error_parse_mixin.dart'; import 'package:agoradesk/features/ads/data/models/ad_model.dart'; +import 'package:agoradesk/features/ads/data/models/ads_request_parameter_model.dart'; +import 'package:agoradesk/features/ads/data/models/asset.dart'; +import 'package:agoradesk/features/ads/data/models/country_code_model.dart'; +import 'package:agoradesk/features/ads/data/models/currency_model.dart'; +import 'package:agoradesk/features/ads/data/models/payment_method_model.dart'; import 'package:agoradesk/features/ads/data/models/trade_type.dart'; import 'package:agoradesk/features/ads/data/repositories/ads_repository.dart'; +import 'package:dropdown_search/dropdown_search.dart'; import 'package:flutter/material.dart'; +import 'package:vm/vm.dart'; class UserAdsViewModel extends ViewModel with ErrorParseMixin, CountryInfoMixin, PaymentMethodsMixin { UserAdsViewModel({ @@ -18,174 +26,272 @@ class UserAdsViewModel extends ViewModel with ErrorParseMixin, CountryInfoMixin, final String username; final AdsRepository _adsRepository; + bool _init = false; + late final TabController tabController; final pageController = PageController(); - final indicatorKeySellTo = GlobalKey(); - final indicatorKeyBuyFrom = GlobalKey(); + final indicatorKey = GlobalKey(); + // final indicatorKeyBuyFrom = GlobalKey(); - final List adsSell = []; - final List adsBuy = []; + final currencyDropdownKey = GlobalKey(); + final countryDropdownKey = GlobalKey(); + + final List ads = []; - TradeType? _tradeType = TradeType.ONLINE_BUY; List tradeTypeMenu = []; + CurrencyModel? selectedCurrency; + CurrencyModel? defaultCurrency; + String? selectedCountryCode; + late CountryCodeModel countryCodeModel; + OnlineProvider? selectedOnlineProvider; + final List _countryPaymentMethods = []; + PaginationMeta? paginationMeta; - PaginationMeta? paginationMetaSell; - PaginationMeta? paginationMetaBuy; - bool hasMorePagesSell = false; - bool hasMorePagesBuy = false; + bool hasMorePages = false; bool _isSellTypeRequest = true; bool _isBuyInitialLoaded = false; bool _isSellInitialLoaded = false; + bool _reloadPaymentMethods = true; - bool _loadingAds = false; - bool buyInitialLoading = true; - bool sellInitialLoading = true; + bool initialLoading = true; + List assetMenu = []; + bool _loadingAds = false; bool get loadingAds => _loadingAds; - set loadingAds(bool v) => updateWith(loadingAds: v); - TradeType? get tradeType => _tradeType; + TradeType? tradeType; - set tradeType(TradeType? v) => updateWith(tradeType: v); + Asset? _asset; + Asset? get asset => _asset; + set asset(Asset? v) => updateWith(asset: v); + + bool _displayFilter = false; + bool get displayFilter => _displayFilter; + set displayFilter(bool v) => updateWith(displayFilter: v); @override void init() async { - tabController.addListener(() { - pageController.animateToPage( - tabController.index, - curve: Curves.linear, - duration: const Duration(milliseconds: 200), - ); - if (tabController.index == 1) { + tabController.addListener(_tabControllerListener); + pageController.addListener(_pageControllerListener); + _initModel(); + super.init(); + } + + Future _initModel() async { + if (_init == false) { + _init = true; + if (GetIt.I().isAgoraDesk == false) { + _asset = Asset.XMR; + } + _initMenus(); + await _loadCaches(); + await onRefresh(); + } + } + + void _initMenus() { + tradeTypeMenu.add('All ads'); + tradeTypeMenu.addAll(TradeType.values.map((e) => e.translatedTitle(context).capitalize()).toList()); + assetMenu.add('All assets'); + assetMenu.addAll(Asset.values.map((e) => e.key())); + } + + Future _tabControllerListener() async { + pageController.animateToPage( + tabController.index, + curve: Curves.linear, + duration: const Duration(milliseconds: 200), + ); + if (tabController.index == 1) { + _loadSellTo(); + } + _isSellTypeRequest = tabController.index == 0; + if (!_isSellTypeRequest && !_isBuyInitialLoaded) { + _isBuyInitialLoaded = true; + indicatorKey.currentState?.show(); + } + notifyListeners(); + } + + Future _pageControllerListener() async { + if (!tabController.indexIsChanging && pageController.page!.round() != tabController.index) { + if (pageController.page!.round() == 1) { _loadSellTo(); } - _isSellTypeRequest = tabController.index == 0; - if (!_isSellTypeRequest && !_isBuyInitialLoaded) { - _isBuyInitialLoaded = true; - indicatorKeySellTo.currentState?.show(); + tabController.animateTo( + pageController.page!.round(), + duration: const Duration(milliseconds: 0), + ); + } + } + + Future _loadCaches() async { + await getCountryCodes(); + await getCurrencies(); + } + + Future> getCountryCodes() async { + _reloadPaymentMethods = true; + final res = await _adsRepository.getCountryCodes(); + if (res.isRight) { + countryCodeModel = res.right; + final List codes = countryCodeModel.codes; + return codes; + } else { + handleApiError(res.left, context); + return ['US']; + } + } + + Future> getCurrencies() async { + _reloadPaymentMethods = true; + final res = await _adsRepository.getCurrencies(); + if (res.isRight) { + final List currencies = []; + currencies.add(kAnyCurrency); + currencies.addAll(res.right.where((e) => e.altcoin != true).toList()); + try { + defaultCurrency = selectedCurrency = res.right.firstWhere((e) => e.code == (selectedCurrency?.code ?? '')); + } catch (e) { + defaultCurrency = null; } notifyListeners(); - }); - pageController.addListener(() { - if (!tabController.indexIsChanging && pageController.page!.round() != tabController.index) { - if (pageController.page!.round() == 1) { - _loadSellTo(); - } - tabController.animateTo( - pageController.page!.round(), - duration: const Duration(milliseconds: 0), - ); + return currencies; + } else { + handleApiError(res.left, context); + return [null]; + } + } + + Future> getCountryPaymentMethods(String country, BuildContext context) async { + if (_reloadPaymentMethods) { + final res = await _adsRepository.getCountryPaymentMethods(country); + if (res.isRight) { + _countryPaymentMethods.clear(); + _countryPaymentMethods.add( + OnlineProvider(url: '', code: kAnyPaymentMethodKey, name: context.intl.any_payment_method, currencies: [])); + _countryPaymentMethods.addAll(res.right); + selectedOnlineProvider = _countryPaymentMethods.first; + _reloadPaymentMethods = false; + notifyListeners(); + return _countryPaymentMethods; + } else { + handleApiError(res.left, context); + return [null]; } - }); - tradeTypeMenu.addAll(TradeType.values.map((e) => e.translatedTitle(context).capitalize()).toList()); - super.init(); + } + return _countryPaymentMethods; } Future _loadSellTo() async { if (!_isSellInitialLoaded) { await Future.delayed(const Duration(milliseconds: 100)); - await indicatorKeySellTo.currentState?.show(); + await indicatorKey.currentState?.show(); _isSellInitialLoaded = true; } } + Future onRefresh() async { + await indicatorKey.currentState?.show(); + } + @override void onAfterBuild() async { - await indicatorKeyBuyFrom.currentState?.show(); + await indicatorKey.currentState?.show(); } Future getAds({bool loadMore = false}) async { if (!loadingAds) { loadingAds = true; + initialLoading = false; - if (_isSellTypeRequest) { - sellInitialLoading = false; - paginationMeta = paginationMetaSell; - } else { - buyInitialLoading = false; - paginationMeta = paginationMetaBuy; - } - - final res1 = await _adsRepository.getUserAds( - username, + final requestParameter = AdsRequestParameterModel( page: loadMore ? (paginationMeta?.currentPage ?? 0) + 1 : 0, - tradeType: _isSellTypeRequest ? TradeType.ONLINE_SELL : TradeType.ONLINE_BUY, - ); - final res2 = await _adsRepository.getUserAds( - username, - page: loadMore ? (paginationMeta?.currentPage ?? 0) + 1 : 0, - tradeType: _isSellTypeRequest ? TradeType.LOCAL_SELL : TradeType.LOCAL_BUY, + tradeType: tradeType, + paymentMethodCode: selectedOnlineProvider?.code == kAnyPaymentMethodKey ? null : selectedOnlineProvider?.code, + currencyCode: selectedCurrency?.name == kAnyCurrency.name ? null : selectedCurrency?.code, + countryCode: selectedCountryCode == kAnyCountryCode ? null : selectedCountryCode, + asset: asset, ); + + final res = await _adsRepository.getUserAds(username, requestParameter); + if (!loadMore) { - if (_isSellTypeRequest) { - adsSell.clear(); - } else { - adsBuy.clear(); - } + ads.clear(); } loadingAds = false; - if (res1.isRight && res2.isRight) { - if (_isSellTypeRequest) { - adsSell.addAll(res1.right.data); - adsSell.addAll(res2.right.data); - } else { - adsBuy.addAll(res1.right.data); - adsBuy.addAll(res2.right.data); - } + if (res.isRight) { + ads.addAll(res.right.data); - if ((res1.right.pagination?.totalPages ?? 0) >= (res1.right.pagination?.totalPages ?? 0)) { - paginationMeta = res1.right.pagination; - if (_isSellTypeRequest) { - paginationMetaSell = res1.right.pagination; - } else { - paginationMetaBuy = res1.right.pagination; - } - } else { - if (_isSellTypeRequest) { - paginationMetaSell = res2.right.pagination; - } else { - paginationMetaBuy = res2.right.pagination; - } + if ((res.right.pagination?.totalPages ?? 0) >= (res.right.pagination?.totalPages ?? 0)) { + paginationMeta = res.right.pagination; } - if (_isSellTypeRequest) { - if (paginationMetaSell != null) { - if (paginationMetaSell!.currentPage < paginationMetaSell!.totalPages) { - hasMorePagesSell = true; - } else { - hasMorePagesSell = false; - } - } - } else { - if (paginationMetaBuy != null) { - if (paginationMetaBuy!.currentPage < paginationMetaBuy!.totalPages) { - hasMorePagesBuy = true; - } else { - hasMorePagesBuy = false; - } + if (paginationMeta != null) { + if (paginationMeta!.currentPage < paginationMeta!.totalPages) { + hasMorePages = true; + } else { + hasMorePages = false; } } } else { - handleApiError(res1.left, context); + handleApiError(res.left, context); } notifyListeners(); } } + Future setTradeType(String? selected) async { + final index = tradeTypeMenu.indexWhere((e) => e == selected); + if (index == 0 || index == -1) { + tradeType = null; + } else { + tradeType = TradeType.values[index - 1]; + } + await onRefresh(); + } + + Future setAsset(String? selected) async { + final index = assetMenu.indexWhere((e) => e == selected); + if (index == 0 || index == -1) { + if (_asset != null) { + _asset = null; + } + } else { + if (_asset != Asset.values[index - 1]) { + _asset = Asset.values[index - 1]; + } + } + await onRefresh(); + } + + void setSelectedCountryCode(String? countryCode) { + selectedCountryCode = countryCode ?? selectedCountryCode; + final currencyCode = getCountryCurrencyCode(selectedCountryCode!); + selectedCurrency = CurrencyModel(code: currencyCode, name: currencyCode, altcoin: true); + currencyDropdownKey.currentState?.changeSelectedItem(selectedCurrency); + notifyListeners(); + } + + void clearFilter() { + selectedCurrency = kAnyCurrency; + selectedCountryCode = kAnyCountryCode; + countryDropdownKey.currentState?.changeSelectedItem(null); + currencyDropdownKey.currentState?.changeSelectedItem(null); + notifyListeners(); + } + void updateWith({ bool? loadingAds, - TradeType? tradeType, + Asset? asset, + bool? displayFilter, }) async { _loadingAds = loadingAds ?? _loadingAds; - _tradeType = tradeType ?? _tradeType; + _asset = asset ?? _asset; + _displayFilter = displayFilter ?? _displayFilter; notifyListeners(); } - - @override - void dispose() { - super.dispose(); - } } diff --git a/lib/features/account/screens/user_ads_screen.dart b/lib/features/account/screens/user_ads_screen.dart index 809651e7..fe134747 100644 --- a/lib/features/account/screens/user_ads_screen.dart +++ b/lib/features/account/screens/user_ads_screen.dart @@ -1,19 +1,30 @@ -import 'package:vm/vm.dart'; +import 'package:agoradesk/core/app_parameters.dart'; import 'package:agoradesk/core/theme/theme.dart'; +import 'package:agoradesk/core/translations/country_info_mixin.dart'; +import 'package:agoradesk/core/translations/payment_method_mixin.dart'; import 'package:agoradesk/core/widgets/branded/agora_appbar.dart'; +import 'package:agoradesk/core/widgets/branded/button_filled_p80.dart'; +import 'package:agoradesk/core/widgets/branded/button_outlined_p80.dart'; +import 'package:agoradesk/core/widgets/branded/header_shadow.dart'; import 'package:agoradesk/core/widgets/branded/no_search_results.dart'; import 'package:agoradesk/features/account/models/user_ads_view_model.dart'; import 'package:agoradesk/features/ads/data/models/ad_model.dart'; +import 'package:agoradesk/features/ads/data/models/currency_model.dart'; +import 'package:agoradesk/features/ads/data/models/payment_method_model.dart'; import 'package:agoradesk/features/ads/data/repositories/ads_repository.dart'; import 'package:agoradesk/features/market/screens/widgets/ad_market_tile.dart'; -import 'package:agoradesk/features/trades/screens/widgets/agora_two_tabs_bar.dart'; +import 'package:agoradesk/features/market/screens/widgets/drop_down_asset_line_with_icons.dart'; +import 'package:agoradesk/features/market/screens/widgets/filter_button.dart'; +import 'package:agoradesk/features/trades/screens/widgets/drop_down_asset_string_line_with_icons.dart'; import 'package:agoradesk/generated/i18n.dart'; import 'package:agoradesk/router.gr.dart'; import 'package:auto_route/auto_route.dart'; +import 'package:dropdown_search/dropdown_search.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:visibility_detector/visibility_detector.dart'; +import 'package:vm/vm.dart'; class UserAdsScreen extends StatefulWidget { const UserAdsScreen({ @@ -29,7 +40,8 @@ class UserAdsScreen extends StatefulWidget { State createState() => _UserAdsScreenState(); } -class _UserAdsScreenState extends State with TickerProviderStateMixin { +class _UserAdsScreenState extends State + with TickerProviderStateMixin, CountryInfoMixin, PaymentMethodsMixin { late final UserAdsViewModel _model; @override @@ -48,59 +60,45 @@ class _UserAdsScreenState extends State with TickerProviderStateM appBar: AgoraAppBar( title: I18n.of(context)!.active_ads, ), - body: Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), - child: LayoutBuilder(builder: (context, constraints) { - return SizedBox( - height: constraints.maxHeight, - child: ViewModelBuilder( - model: _model, - builder: (context, model, child) { - return Column( - children: [ - // _buildSelectAdType(context, model), - AgoraTwoTabsBar( - controller: model.tabController, - textLeft: context.intl.app_buy_crypto, - textRight: context.intl.app_sell_crypto, - disable: model.loadingAds, - ), - const SizedBox(height: 20), - // Expanded( - // child: _buildAdsList(context, model), - // ), - Expanded( - child: PageView( - controller: model.pageController, - physics: const ClampingScrollPhysics(), - children: [ - _buildBuyFrom(context, model), // buy from this user - _buildSellTo(context, model), // sell to this user - ], - ), + body: LayoutBuilder(builder: (context, constraints) { + return SizedBox( + height: constraints.maxHeight, + child: ViewModelBuilder( + model: _model, + builder: (context, model, child) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(4, 0, 4, 0), + child: _buildTopFilter(context, model), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), + child: _buildBuyFrom(context, model), ), - ], - ); - }), - ); - }), - ), + ), + ], + ); + }), + ); + }), ); } Widget _buildBuyFrom(BuildContext context, UserAdsViewModel model) { return RefreshIndicator( - key: model.indicatorKeyBuyFrom, + key: model.indicatorKey, onRefresh: model.getAds, child: LayoutBuilder(builder: (context, constraints) { return ListView.builder( shrinkWrap: true, - itemCount: model.adsSell.length + 1, + itemCount: model.ads.length + 1, itemBuilder: (context, index) { - if (model.adsSell.isEmpty) { + if (model.ads.isEmpty) { return ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: model.loadingAds || model.sellInitialLoading + child: model.loadingAds || model.initialLoading ? const SizedBox() : NoSearchResults( text: context.intl.dashboard250Sbads250Sbfilter250Sbno8722Sbads, @@ -108,18 +106,18 @@ class _UserAdsScreenState extends State with TickerProviderStateM ); } - if (index < model.adsSell.length) { + if (index < model.ads.length) { return Padding( padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), child: AdMarketTile( - ad: model.adsSell[index], + ad: model.ads[index], onPressed: () async { - await AutoRouter.of(context).push(MarketAdInfoRoute(ad: model.adsSell[index])); + await AutoRouter.of(context).push(MarketAdInfoRoute(ad: model.ads[index])); }, ), ); } else { - return model.hasMorePagesSell + return model.hasMorePages ? VisibilityDetector( key: UniqueKey(), onVisibilityChanged: (VisibilityInfo info) { @@ -142,57 +140,300 @@ class _UserAdsScreenState extends State with TickerProviderStateM ); } - Widget _buildSellTo(BuildContext context, UserAdsViewModel model) { - return RefreshIndicator( - key: model.indicatorKeySellTo, - onRefresh: model.getAds, - child: LayoutBuilder(builder: (context, constraints) { - return ListView.builder( - shrinkWrap: true, - itemCount: model.adsBuy.length + 1, - itemBuilder: (context, index) { - if (model.adsBuy.isEmpty) { - return ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: model.loadingAds || model.buyInitialLoading - ? const SizedBox() - : NoSearchResults( - text: context.intl.dashboard250Sbads250Sbfilter250Sbno8722Sbads, - ), - ); - } + // Widget _buildSellTo(BuildContext context, UserAdsViewModel model) { + // return RefreshIndicator( + // key: model.indicatorKey, + // onRefresh: model.getAds, + // child: LayoutBuilder(builder: (context, constraints) { + // return ListView.builder( + // shrinkWrap: true, + // itemCount: model.adsBuy.length + 1, + // itemBuilder: (context, index) { + // if (model.adsBuy.isEmpty) { + // return ConstrainedBox( + // constraints: BoxConstraints(minHeight: constraints.maxHeight), + // child: model.loadingAds || model.buyInitialLoading + // ? const SizedBox() + // : NoSearchResults( + // text: context.intl.dashboard250Sbads250Sbfilter250Sbno8722Sbads, + // ), + // ); + // } - if (index < model.adsBuy.length) { - return Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), - child: AdMarketTile( - ad: model.adsBuy[index], - onPressed: () async { - await AutoRouter.of(context).push(MarketAdInfoRoute(ad: model.adsBuy[index])); + // if (index < model.adsBuy.length) { + // return Padding( + // padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), + // child: AdMarketTile( + // ad: model.adsBuy[index], + // onPressed: () async { + // await AutoRouter.of(context).push(MarketAdInfoRoute(ad: model.adsBuy[index])); + // }, + // ), + // ); + // } else { + // return model.hasMorePagesBuy + // ? VisibilityDetector( + // key: UniqueKey(), + // onVisibilityChanged: (VisibilityInfo info) { + // if (info.visibleFraction > 0.1) { + // model.getAds(loadMore: true); + // } + // }, + // child: const SizedBox( + // height: 80, + // child: Center( + // child: CupertinoActivityIndicator(), + // ), + // ), + // ) + // : const SizedBox(); + // } + // }, + // ); + // }), + // ); + // } + + Widget _buildTopFilter(BuildContext context, UserAdsViewModel model) { + return HeaderShadow( + children: [ + Row( + children: [ + /// trade type dropdown + Expanded( + flex: 1, + child: Semantics( + label: context.intl.app_select_trade_type, + child: DropdownSearch( + dropdownButtonProps: context.dropdownButtonProps(label: context.intl.app_select_trade_type), + dropdownDecoratorProps: context.dropdownDecoration, + popupProps: PopupProps.menu( + menuProps: context.dropdownMenuProps, + fit: FlexFit.loose, + itemBuilder: (context, val, isSelected) { + return DropdownAssetLineWithIcon( + name: val, + ); + }, + ), + items: model.tradeTypeMenu, + onChanged: model.setTradeType, + selectedItem: model.tradeTypeMenu[0], + dropdownBuilder: (context, val) { + return DropdownAssetLineWithIcon( + name: val!, + padding: const EdgeInsets.all(0), + ); }, ), - ); - } else { - return model.hasMorePagesBuy - ? VisibilityDetector( - key: UniqueKey(), - onVisibilityChanged: (VisibilityInfo info) { - if (info.visibleFraction > 0.1) { - model.getAds(loadMore: true); - } - }, - child: const SizedBox( - height: 80, - child: Center( - child: CupertinoActivityIndicator(), - ), + ), + ), + + /// asset dropdown + GetIt.I().isAgoraDesk ? const SizedBox(width: 6) : const SizedBox(), + GetIt.I().isAgoraDesk + ? Expanded( + flex: 1, + child: DropdownSearch( + dropdownButtonProps: context.dropdownButtonProps(), + dropdownDecoratorProps: context.dropdownDecoration, + popupProps: PopupProps.menu( + menuProps: context.dropdownMenuProps, + fit: FlexFit.loose, + itemBuilder: (context, assetStr, isSelected) { + return DropdownAssetStringLineWithIcon( + name: assetStr, + ); + }, ), - ) - : const SizedBox(); - } - }, - ); - }), + items: model.assetMenu, + onChanged: model.setAsset, + selectedItem: model.assetMenu[0], + dropdownBuilder: (context, assetStr) { + return DropdownAssetStringLineWithIcon( + name: assetStr ?? '', + padding: const EdgeInsets.all(0), + ); + }, + ), + ) + : const SizedBox(), + Padding( + padding: const EdgeInsets.fromLTRB(6, 0, 0, 0), + child: FilterButton( + selected: model.displayFilter, + onPressed: () => _buildExpandedFilter(context, model), + ), + ), + ], + ), + ], ); } + + void _buildExpandedFilter(BuildContext context, UserAdsViewModel model) { + final widthHalf = MediaQuery.of(context).size.width / 2 - 18; + const marginTextList = 4.0; + + const radius = Radius.circular(20); + final height = MediaQuery.of(context).size.height - 70; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + enableDrag: true, + constraints: BoxConstraints(maxHeight: height), + clipBehavior: Clip.antiAlias, + backgroundColor: Colors.transparent, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only(topLeft: radius, topRight: radius), + ), + builder: (context) { + return ViewModelBuilder( + model: model, + disposable: false, + builder: (context, model, child) { + return Container( + color: context.colSurf4Surf1, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 20, 16, 20), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + Text( + context.intl.dashboard250Sbads250Sbfilter250Sbsort, + style: context.txtBodySmallN60, + ), + const SizedBox(height: marginTextList), + + /// country dropdown + Text( + context.intl.post8722Sbad250Sbcountry250Sbtitle, + style: context.txtBodySmallN60, + ), + const SizedBox(height: marginTextList), + DropdownSearch( + key: model.countryDropdownKey, + dropdownButtonProps: context.dropdownButtonProps(), + dropdownDecoratorProps: context.dropdownDecoration, + popupProps: PopupProps.dialog( + dialogProps: context.dropdownDialogProps, + showSearchBox: true, + searchFieldProps: TextFieldProps( + autofocus: true, + decoration: InputDecoration(labelText: context.intl.search250Sbbtn), + ), + ), + itemAsString: (String? code) => getCountryName(code ?? ''), + asyncItems: (String? filter) => model.getCountryCodes(), + selectedItem: model.selectedCountryCode, + onChanged: (val) => model.setSelectedCountryCode(val), + ), + const SizedBox(height: 8), + + /// currency dropdown + Text( + context.intl.guide250Sbtrade250Sbblock8722Sb38722Sbtext8722Sb08722Sbcurrency, + style: context.txtBodySmallN60, + ), + const SizedBox(height: marginTextList), + DropdownSearch( + key: model.currencyDropdownKey, + dropdownButtonProps: context.dropdownButtonProps(), + dropdownDecoratorProps: context.dropdownDecoration, + popupProps: PopupProps.dialog( + dialogProps: context.dropdownDialogProps, + showSearchBox: true, + searchFieldProps: TextFieldProps( + autofocus: true, + decoration: InputDecoration(labelText: context.intl.search250Sbbtn), + ), + ), + itemAsString: (CurrencyModel? currency) => currency?.code ?? '', + asyncItems: (String? filter) => model.getCurrencies(), + selectedItem: model.selectedCurrency, + onChanged: (val) => model.selectedCurrency = val, + ), + + /// payment method dropdown + const SizedBox(height: 8), + Text( + context.intl.guide250Sbtrade250Sbblock8722Sb38722Sbtext8722Sb08722Sbpayment8722Sbmethod, + style: context.txtBodySmallN60, + ), + const SizedBox(height: marginTextList), + Semantics( + label: context.intl.app_select_payment_method, + child: DropdownSearch( + dropdownButtonProps: + context.dropdownButtonProps(label: context.intl.app_select_payment_method), + dropdownDecoratorProps: context.dropdownDecoration, + popupProps: PopupProps.dialog( + dialogProps: context.dropdownDialogProps, + showSearchBox: true, + searchFieldProps: TextFieldProps( + autofocus: true, + decoration: InputDecoration(labelText: context.intl.search250Sbbtn), + ), + itemBuilder: (context, val, isSelected) { + return DropdownAssetLineWithIcon( + name: val?.name ?? '', + svgPath: getPaymentMethodIconPath(val?.code), + ); + }, + ), + asyncItems: (String? filter) => + model.getCountryPaymentMethods(model.selectedCountryCode ?? '', context), + onChanged: (val) => model.selectedOnlineProvider = val, + selectedItem: model.selectedOnlineProvider, + dropdownBuilder: (context, val) { + return DropdownAssetLineWithIcon( + name: val?.name ?? '', + svgPath: getPaymentMethodIconPath(val?.code), + padding: const EdgeInsets.all(0), + ); + }, + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: widthHalf - 4, + child: Center( + child: ButtonOutlinedP80( + title: context.intl.clear_all, + minimumSize: const Size.fromHeight(40), + onPressed: () { + model.clearFilter(); + }, + ), + ), + ), + SizedBox( + width: widthHalf - 4, + child: ButtonFilledP80( + title: context.intl.apply, + onPressed: () async { + await model.onRefresh(); + model.displayFilter = false; + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + const SizedBox(height: 40), + ], + ), + ), + ), + ); + }); + }); + } } diff --git a/lib/features/ads/data/repositories/ads_repository.dart b/lib/features/ads/data/repositories/ads_repository.dart index 41077281..b4352662 100644 --- a/lib/features/ads/data/repositories/ads_repository.dart +++ b/lib/features/ads/data/repositories/ads_repository.dart @@ -141,14 +141,12 @@ class AdsRepository with CountryInfoMixin { /// Get ads by username /// Future>> getUserAds( - String username, { - int? page, - TradeType? tradeType, - }) async { + String username, + AdsRequestParameterModel requestParameter, + ) async { return _adsService.getUserAds( username, - page: page, - tradeType: tradeType, + requestParameter, ); } diff --git a/lib/features/ads/data/services/ads_service.dart b/lib/features/ads/data/services/ads_service.dart index 65784010..e510f1b6 100644 --- a/lib/features/ads/data/services/ads_service.dart +++ b/lib/features/ads/data/services/ads_service.dart @@ -196,22 +196,13 @@ class AdsService { /// Get ads by username /// Future>> getUserAds( - String username, { - int? page, - TradeType? tradeType, - }) async { + String username, + AdsRequestParameterModel requestParameter, + ) async { try { - final Map parameters = {}; - String urlParameter = ''; - if (page != null) { - urlParameter = '?page=$page'; - } - if (tradeType != null) { - parameters['trade_type'] = tradeType.name; - } final resp = await _api.client.get( - '/user-ads/$username$urlParameter', - queryParameters: parameters, + '/user-ads/$username', + queryParameters: requestParameter.toJson(), ); if (resp.statusCode == 200) { List respMap = jsonDecode(jsonEncode(resp.data['data']['ad_list'])); diff --git a/lib/features/ads/models/ads_view_model.dart b/lib/features/ads/models/ads_view_model.dart index b599c316..4d4fcd21 100644 --- a/lib/features/ads/models/ads_view_model.dart +++ b/lib/features/ads/models/ads_view_model.dart @@ -83,7 +83,6 @@ class AdsViewModel extends ViewModel with ErrorParseMixin, CountryInfoMixin, Val bool _changingVisibility = false; - Asset? _asset; SortingDirectionType _sortingDirectionType = SortingDirectionType.asc; final List ads = []; final List selectedAdIds = []; @@ -98,7 +97,7 @@ class AdsViewModel extends ViewModel with ErrorParseMixin, CountryInfoMixin, Val TradeType? tradeType; UserSettingsModel userSettingsModel = UserSettingsModel(); late bool isGuestMode; - bool _displayFilter = false; + bool? _selVisibility; AgoraMenuItem? dropdownValue; @@ -167,8 +166,8 @@ class AdsViewModel extends ViewModel with ErrorParseMixin, CountryInfoMixin, Val set deletingAds(bool v) => updateWith(deletingAds: v); + bool _displayFilter = false; bool get displayFilter => _displayFilter; - set displayFilter(bool v) => updateWith(displayFilter: v); bool get loadingSettings => _loadingSettings; @@ -191,8 +190,8 @@ class AdsViewModel extends ViewModel with ErrorParseMixin, CountryInfoMixin, Val set isBulkActionsMode(bool v) => updateWith(isBulkActionsMode: v); + Asset? _asset; Asset? get asset => _asset; - set asset(Asset? v) => updateWith(asset: v); SortingDirectionType get sortingDirectionType => _sortingDirectionType; @@ -208,18 +207,18 @@ class AdsViewModel extends ViewModel with ErrorParseMixin, CountryInfoMixin, Val _authService.onAuthStateChange.listen((e) { isGuestMode = e == AuthState.guest; if (e == AuthState.loggedIn) { - initModel(); + _initModel(); indicatorKey.currentState?.show(); } notifyListeners(); }); if (_authService.isAuthenticated) { - initModel(); + _initModel(); } super.init(); } - void initModel() { + void _initModel() { if (_init == false) { _init = true; if (GetIt.I().isAgoraDesk == false) { @@ -333,7 +332,7 @@ class AdsViewModel extends ViewModel with ErrorParseMixin, CountryInfoMixin, Val notifyListeners(); } - Future> getCountryPaymentMethods(String country) async { + Future> getCountryPaymentMethods(String country, BuildContext context) async { if (_reloadPaymentMethods) { final res = await _adsRepository.getCountryPaymentMethods(country); if (res.isRight) { @@ -493,18 +492,20 @@ class AdsViewModel extends ViewModel with ErrorParseMixin, CountryInfoMixin, Val tradeType = TradeType.values[index - 1]; } indicatorKey.currentState?.show(); - // notifyListeners(); } void setAsset(String? selected) { final index = assetMenu.indexWhere((e) => e == selected); if (index == 0 || index == -1) { - _asset = null; + if (_asset != null) { + _asset = null; + } } else { - _asset = Asset.values[index - 1]; + if (_asset != Asset.values[index - 1]) { + _asset = Asset.values[index - 1]; + } } indicatorKey.currentState?.show(); - // notifyListeners(); } void _ctrlListeners() { diff --git a/lib/features/ads/screens/ads_screen.dart b/lib/features/ads/screens/ads_screen.dart index ef7ad47b..6333e81c 100644 --- a/lib/features/ads/screens/ads_screen.dart +++ b/lib/features/ads/screens/ads_screen.dart @@ -689,7 +689,7 @@ class _AdsScreenState extends State with TickerProviderStateMixin, Co }, ), asyncItems: (String? filter) => - model.getCountryPaymentMethods(model.selectedCountryCode ?? ''), + model.getCountryPaymentMethods(model.selectedCountryCode ?? '', context), onChanged: (val) => model.selectedOnlineProvider = val, selectedItem: model.selectedOnlineProvider, dropdownBuilder: (context, val) { diff --git a/lib/features/profile/models/my_profile_view_model.dart b/lib/features/profile/models/my_profile_view_model.dart index e1b58ee2..1ee1296e 100644 --- a/lib/features/profile/models/my_profile_view_model.dart +++ b/lib/features/profile/models/my_profile_view_model.dart @@ -8,6 +8,7 @@ import 'package:agoradesk/features/account/data/models/account_info_model.dart'; import 'package:agoradesk/features/account/data/models/feedback_model.dart'; import 'package:agoradesk/features/account/data/services/account_service.dart'; import 'package:agoradesk/features/ads/data/models/ad_model.dart'; +import 'package:agoradesk/features/ads/data/models/ads_request_parameter_model.dart'; import 'package:agoradesk/features/ads/data/repositories/ads_repository.dart'; import 'package:agoradesk/features/auth/data/services/auth_service.dart'; import 'package:agoradesk/features/profile/data/models/user_settings_model.dart'; @@ -120,7 +121,10 @@ class MyProfileViewModel extends ViewModel with ValidatorMixin, ErrorParseMixin Future _getAds() async { loadingAds = true; ads.clear(); - final res = await _adsRepository.getUserAds(username); + final res = await _adsRepository.getUserAds( + username, + const AdsRequestParameterModel(), + ); loadingAds = false; if (res.isRight) { paginationMeta = res.right.pagination; diff --git a/lib/features/trades/models/trade_view_model.dart b/lib/features/trades/models/trade_view_model.dart index e82585da..89d843d3 100644 --- a/lib/features/trades/models/trade_view_model.dart +++ b/lib/features/trades/models/trade_view_model.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'dart:ui'; import 'package:agoradesk/core/api/api_client.dart'; @@ -49,7 +48,7 @@ import 'package:vm/vm.dart'; import 'note_on_user_view_model.dart'; /// Polling trade activity and new messages in the chat when the trade screen is open -const _kPollingSeconds = 60; +const _kPollingSeconds = 90; const kDeletedUserName = '[DELETED]'; @@ -301,11 +300,15 @@ class TradeViewModel extends ViewModel await Future.delayed(const Duration(milliseconds: 300)); isTradeLoading = false; - _timer?.cancel(); - _timer = Timer.periodic(const Duration(seconds: _kPollingSeconds), (_) async => _polling()); + if (_timer?.isActive != true) { + _timer = Timer.periodic(const Duration(seconds: _kPollingSeconds), (_) async => _polling()); + } } Future _polling() async { + if (GetIt.I().polling) { + return; + } if (GetIt.I().includeFcm == false || GetIt.I().isGoogleAvailable == false || isProcessing()) { @@ -313,12 +316,16 @@ class TradeViewModel extends ViewModel await indicatorKey.currentState?.show(); _calcTimeBeforeCancel(); await _getMessages(polling: true); - Future.delayed(const Duration(milliseconds: 500)).then((value) => GetIt.I().polling = false); + await Future.delayed(const Duration(milliseconds: 500)); + GetIt.I().polling = false; } } _listenEventBus() { _updateOpenedChatSubscription = eventBus.on().listen((e) async { + if (GetIt.I().polling) { + return; + } GetIt.I().polling = true; await indicatorKey.currentState?.show(); _calcTimeBeforeCancel(); @@ -425,33 +432,35 @@ class TradeViewModel extends ViewModel } Future getTrade({bool polling = false}) async { - if (!_pollingLoading) { - late final String tradeIdWithPolling; - if (polling) { - _pollingLoading = true; - if (tradeId != null) { - tradeIdWithPolling = tradeId!; - } else { - tradeIdWithPolling = tradeForScreen.tradeId; - } - } else { + if (_pollingLoading) { + return; + } + + late final String tradeIdWithPolling; + if (polling) { + _pollingLoading = true; + if (tradeId != null) { tradeIdWithPolling = tradeId!; - } - final res = await _tradeRepository.getTrade(id: tradeIdWithPolling); - _pollingLoading = false; - if (res.isRight) { - errorTradeLoading = false; - tradeForScreen = res.right; - if (polling) { - await _setTradeStatus(); - _calcTimeBeforeCancel(); - notifyListeners(); - } } else { - if (!polling) { - errorTradeLoading = true; - handleApiError(res.left, context); - } + tradeIdWithPolling = tradeForScreen.tradeId; + } + } else { + tradeIdWithPolling = tradeId!; + } + final res = await _tradeRepository.getTrade(id: tradeIdWithPolling); + _pollingLoading = false; + if (res.isRight) { + errorTradeLoading = false; + tradeForScreen = res.right; + if (polling) { + await _setTradeStatus(); + _calcTimeBeforeCancel(); + notifyListeners(); + } + } else { + if (!polling) { + errorTradeLoading = true; + handleApiError(res.left, context); } } } @@ -906,6 +915,7 @@ class TradeViewModel extends ViewModel } } } + await Future.delayed(const Duration(milliseconds: 1000)); _gettingMessages = false; } @@ -1269,7 +1279,6 @@ class TradeViewModel extends ViewModel void didChangeAppLifecycleState(AppLifecycleState state) async { if (state == AppLifecycleState.resumed) { _calcTimeBeforeCancel(); - await indicatorKey.currentState?.show(); await _getMessages(polling: true); } } diff --git a/lib/features/trades/screens/widgets/chat_image.dart b/lib/features/trades/screens/widgets/chat_image.dart index f420bb8d..38475cc9 100644 --- a/lib/features/trades/screens/widgets/chat_image.dart +++ b/lib/features/trades/screens/widgets/chat_image.dart @@ -29,6 +29,8 @@ class ChatImage extends StatelessWidget { width: 160, child: InstaImageViewer( disableSwipeToDismiss: true, + imageUrl: message.attachmentUrl ?? '', + headers: headers, child: ClipRRect( borderRadius: BorderRadius.circular(4), child: message.isSending || message.isUpdated diff --git a/pubspec.lock b/pubspec.lock index cbf85b85..7ff9d239 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1122,10 +1122,10 @@ packages: dependency: "direct main" description: name: insta_image_viewer - sha256: "9a81432b1ab907ea91c255ec079440afe43f999533422badffaa482dfb7fdfbb" + sha256: "5c3150415d19945ee7eb9a986ed636219e384164e53a4854e8c6f3dbb3b3e197" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.4" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7b0e51fd..29548f91 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: agoradesk -version: 1.1.33+133 +version: 1.1.34+134 publish_to: none description: Person-to-person platform to allow anyone to trade their local currency for cryptocurrency. environment: @@ -50,7 +50,7 @@ dependencies: image_picker: ^1.0.7 in_app_review: ^2.0.9 injectable: ^2.1.0 - insta_image_viewer: ^1.0.2 + insta_image_viewer: ^1.0.4 intl: any json_annotation: ^4.4.0 local_auth: ^2.1.8