✨ Fund creation and attach found to message
This commit is contained in:
@@ -1329,5 +1329,9 @@
|
|||||||
"more": "More",
|
"more": "More",
|
||||||
"collapse": "Collapse",
|
"collapse": "Collapse",
|
||||||
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
||||||
"discard": "Discard"
|
"discard": "Discard",
|
||||||
|
"fund": "Fund",
|
||||||
|
"fundsRecent": "Recent Funds",
|
||||||
|
"fundCreateNew": "Create New",
|
||||||
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import "package:island/database/message.dart";
|
|||||||
import "package:island/models/chat.dart";
|
import "package:island/models/chat.dart";
|
||||||
import "package:island/models/file.dart";
|
import "package:island/models/file.dart";
|
||||||
import "package:island/models/poll.dart";
|
import "package:island/models/poll.dart";
|
||||||
|
import "package:island/models/wallet.dart";
|
||||||
import "package:island/pods/database.dart";
|
import "package:island/pods/database.dart";
|
||||||
import "package:island/pods/lifecycle.dart";
|
import "package:island/pods/lifecycle.dart";
|
||||||
import "package:island/pods/network.dart";
|
import "package:island/pods/network.dart";
|
||||||
@@ -439,6 +440,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
String content,
|
String content,
|
||||||
List<UniversalFile> attachments, {
|
List<UniversalFile> attachments, {
|
||||||
SnPoll? poll,
|
SnPoll? poll,
|
||||||
|
SnWalletFund? fund,
|
||||||
SnChatMessage? editingTo,
|
SnChatMessage? editingTo,
|
||||||
SnChatMessage? forwardingTo,
|
SnChatMessage? forwardingTo,
|
||||||
SnChatMessage? replyingTo,
|
SnChatMessage? replyingTo,
|
||||||
@@ -501,6 +503,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
'replied_message_id': replyingTo?.id,
|
'replied_message_id': replyingTo?.id,
|
||||||
'forwarded_message_id': forwardingTo?.id,
|
'forwarded_message_id': forwardingTo?.id,
|
||||||
'poll_id': poll?.id,
|
'poll_id': poll?.id,
|
||||||
|
'fund_id': fund?.id,
|
||||||
'meta': {},
|
'meta': {},
|
||||||
'nonce': nonce,
|
'nonce': nonce,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import "package:island/database/message.dart";
|
|||||||
import "package:island/models/chat.dart";
|
import "package:island/models/chat.dart";
|
||||||
import "package:island/models/file.dart";
|
import "package:island/models/file.dart";
|
||||||
import "package:island/models/poll.dart";
|
import "package:island/models/poll.dart";
|
||||||
|
import "package:island/models/wallet.dart";
|
||||||
import "package:island/pods/chat/chat_rooms.dart";
|
import "package:island/pods/chat/chat_rooms.dart";
|
||||||
import "package:island/pods/chat/chat_subscribe.dart";
|
import "package:island/pods/chat/chat_subscribe.dart";
|
||||||
import "package:island/pods/chat/messages_notifier.dart";
|
import "package:island/pods/chat/messages_notifier.dart";
|
||||||
@@ -172,6 +173,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final messageForwardingTo = useState<SnChatMessage?>(null);
|
final messageForwardingTo = useState<SnChatMessage?>(null);
|
||||||
final messageEditingTo = useState<SnChatMessage?>(null);
|
final messageEditingTo = useState<SnChatMessage?>(null);
|
||||||
final selectedPoll = useState<SnPoll?>(null);
|
final selectedPoll = useState<SnPoll?>(null);
|
||||||
|
final selectedFund = useState<SnWalletFund?>(null);
|
||||||
final attachments = useState<List<UniversalFile>>([]);
|
final attachments = useState<List<UniversalFile>>([]);
|
||||||
final attachmentProgress = useState<Map<String, Map<int, double?>>>({});
|
final attachmentProgress = useState<Map<String, Map<int, double?>>>({});
|
||||||
|
|
||||||
@@ -288,12 +290,14 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
void sendMessage() {
|
void sendMessage() {
|
||||||
if (messageController.text.trim().isNotEmpty ||
|
if (messageController.text.trim().isNotEmpty ||
|
||||||
attachments.value.isNotEmpty ||
|
attachments.value.isNotEmpty ||
|
||||||
selectedPoll.value != null) {
|
selectedPoll.value != null ||
|
||||||
|
selectedFund.value != null) {
|
||||||
messagesNotifier.sendMessage(
|
messagesNotifier.sendMessage(
|
||||||
ref,
|
ref,
|
||||||
messageController.text.trim(),
|
messageController.text.trim(),
|
||||||
attachments.value,
|
attachments.value,
|
||||||
poll: selectedPoll.value,
|
poll: selectedPoll.value,
|
||||||
|
fund: selectedFund.value,
|
||||||
editingTo: messageEditingTo.value,
|
editingTo: messageEditingTo.value,
|
||||||
forwardingTo: messageForwardingTo.value,
|
forwardingTo: messageForwardingTo.value,
|
||||||
replyingTo: messageReplyingTo.value,
|
replyingTo: messageReplyingTo.value,
|
||||||
@@ -309,6 +313,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
messageReplyingTo.value = null;
|
messageReplyingTo.value = null;
|
||||||
messageForwardingTo.value = null;
|
messageForwardingTo.value = null;
|
||||||
selectedPoll.value = null;
|
selectedPoll.value = null;
|
||||||
|
selectedFund.value = null;
|
||||||
attachments.value = [];
|
attachments.value = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1252,12 +1257,15 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
messageReplyingTo.value = null;
|
messageReplyingTo.value = null;
|
||||||
messageForwardingTo.value = null;
|
messageForwardingTo.value = null;
|
||||||
selectedPoll.value = null;
|
selectedPoll.value = null;
|
||||||
|
selectedFund.value = null;
|
||||||
},
|
},
|
||||||
messageEditingTo: messageEditingTo.value,
|
messageEditingTo: messageEditingTo.value,
|
||||||
messageReplyingTo: messageReplyingTo.value,
|
messageReplyingTo: messageReplyingTo.value,
|
||||||
messageForwardingTo: messageForwardingTo.value,
|
messageForwardingTo: messageForwardingTo.value,
|
||||||
selectedPoll: selectedPoll.value,
|
selectedPoll: selectedPoll.value,
|
||||||
onPollSelected: (poll) => selectedPoll.value = poll,
|
onPollSelected: (poll) => selectedPoll.value = poll,
|
||||||
|
selectedFund: selectedFund.value,
|
||||||
|
onFundSelected: (fund) => selectedFund.value = fund,
|
||||||
onPickFile: (bool isPhoto) {
|
onPickFile: (bool isPhoto) {
|
||||||
if (isPhoto) {
|
if (isPhoto) {
|
||||||
pickPhotoMedia();
|
pickPhotoMedia();
|
||||||
|
|||||||
@@ -237,7 +237,9 @@ class PostSearchScreen extends HookConsumerWidget {
|
|||||||
controller: pubNameController,
|
controller: pubNameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'pubName'.tr(),
|
labelText: 'pubName'.tr(),
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onChanged:
|
onChanged:
|
||||||
(value) => onSearchWithFilters(searchController.text),
|
(value) => onSearchWithFilters(searchController.text),
|
||||||
@@ -247,7 +249,9 @@ class PostSearchScreen extends HookConsumerWidget {
|
|||||||
controller: realmController,
|
controller: realmController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'realm'.tr(),
|
labelText: 'realm'.tr(),
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onChanged:
|
onChanged:
|
||||||
(value) => onSearchWithFilters(searchController.text),
|
(value) => onSearchWithFilters(searchController.text),
|
||||||
|
|||||||
@@ -103,17 +103,9 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
|
|||||||
labelText: 'enterAmount'.tr(),
|
labelText: 'enterAmount'.tr(),
|
||||||
hintText: '0.00',
|
hintText: '0.00',
|
||||||
prefixIcon: Icon(kCurrencyIconData[selectedCurrency]),
|
prefixIcon: Icon(kCurrencyIconData[selectedCurrency]),
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
enabledBorder: OutlineInputBorder(
|
borderRadius: const BorderRadius.all(
|
||||||
borderSide: BorderSide(
|
Radius.circular(12),
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.outline.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -136,17 +128,9 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
|
|||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: selectedCurrency,
|
value: selectedCurrency,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
enabledBorder: OutlineInputBorder(
|
borderRadius: const BorderRadius.all(
|
||||||
borderSide: BorderSide(
|
Radius.circular(12),
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.outline.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -370,17 +354,9 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
|
|||||||
labelText: 'personalMessage'.tr(),
|
labelText: 'personalMessage'.tr(),
|
||||||
hintText: 'addPersonalMessageForRecipients'.tr(),
|
hintText: 'addPersonalMessageForRecipients'.tr(),
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
enabledBorder: OutlineInputBorder(
|
borderRadius: const BorderRadius.all(
|
||||||
borderSide: BorderSide(
|
Radius.circular(12),
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.outline.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -526,11 +502,6 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedRecipients.isEmpty) {
|
|
||||||
showErrorAlert('noRecipientsSelected'.tr());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = {
|
final data = {
|
||||||
'currency': selectedCurrency,
|
'currency': selectedCurrency,
|
||||||
'total_amount': amount,
|
'total_amount': amount,
|
||||||
@@ -610,17 +581,9 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
|
|||||||
labelText: 'enterAmount'.tr(),
|
labelText: 'enterAmount'.tr(),
|
||||||
hintText: '0.00',
|
hintText: '0.00',
|
||||||
prefixIcon: Icon(kCurrencyIconData[selectedCurrency]),
|
prefixIcon: Icon(kCurrencyIconData[selectedCurrency]),
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
enabledBorder: OutlineInputBorder(
|
borderRadius: const BorderRadius.all(
|
||||||
borderSide: BorderSide(
|
Radius.circular(12),
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.outline.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -643,17 +606,9 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
|
|||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: selectedCurrency,
|
value: selectedCurrency,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
enabledBorder: OutlineInputBorder(
|
borderRadius: const BorderRadius.all(
|
||||||
borderSide: BorderSide(
|
Radius.circular(12),
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.outline.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -817,17 +772,9 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
|
|||||||
labelText: 'transferRemark'.tr(),
|
labelText: 'transferRemark'.tr(),
|
||||||
hintText: 'addRemarkForTransfer'.tr(),
|
hintText: 'addRemarkForTransfer'.tr(),
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
enabledBorder: OutlineInputBorder(
|
borderRadius: const BorderRadius.all(
|
||||||
borderSide: BorderSide(
|
Radius.circular(12),
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.outline.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -240,7 +240,11 @@ class _PurchaseGiftSheetState extends State<PurchaseGiftSheet> {
|
|||||||
labelText: 'personalMessage'.tr(),
|
labelText: 'personalMessage'.tr(),
|
||||||
hintText: 'addPersonalMessageForRecipient'.tr(),
|
hintText: 'addPersonalMessageForRecipient'.tr(),
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
@@ -925,7 +929,9 @@ class StellarProgramTab extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
hintText: 'enterGiftCode'.tr(),
|
hintText: 'enterGiftCode'.tr(),
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import "package:island/models/chat.dart";
|
|||||||
import "package:island/models/file.dart";
|
import "package:island/models/file.dart";
|
||||||
import "package:island/models/poll.dart";
|
import "package:island/models/poll.dart";
|
||||||
import "package:island/models/publisher.dart";
|
import "package:island/models/publisher.dart";
|
||||||
|
import "package:island/models/wallet.dart";
|
||||||
import "package:island/models/realm.dart";
|
import "package:island/models/realm.dart";
|
||||||
import "package:island/models/sticker.dart";
|
import "package:island/models/sticker.dart";
|
||||||
import "package:island/pods/config.dart";
|
import "package:island/pods/config.dart";
|
||||||
@@ -28,6 +29,7 @@ import "package:material_symbols_icons/symbols.dart";
|
|||||||
import "package:island/widgets/stickers/sticker_picker.dart";
|
import "package:island/widgets/stickers/sticker_picker.dart";
|
||||||
import "package:island/pods/chat/chat_subscribe.dart";
|
import "package:island/pods/chat/chat_subscribe.dart";
|
||||||
import "package:island/widgets/post/compose_poll.dart";
|
import "package:island/widgets/post/compose_poll.dart";
|
||||||
|
import "package:island/widgets/post/compose_fund.dart";
|
||||||
|
|
||||||
void _insertPlaceholder(TextEditingController controller, String placeholder) {
|
void _insertPlaceholder(TextEditingController controller, String placeholder) {
|
||||||
final text = controller.text;
|
final text = controller.text;
|
||||||
@@ -47,11 +49,15 @@ class _ExpandedSection extends StatelessWidget {
|
|||||||
final TextEditingController messageController;
|
final TextEditingController messageController;
|
||||||
final SnPoll? selectedPoll;
|
final SnPoll? selectedPoll;
|
||||||
final Function(SnPoll?) onPollSelected;
|
final Function(SnPoll?) onPollSelected;
|
||||||
|
final SnWalletFund? selectedFund;
|
||||||
|
final Function(SnWalletFund?) onFundSelected;
|
||||||
|
|
||||||
const _ExpandedSection({
|
const _ExpandedSection({
|
||||||
required this.messageController,
|
required this.messageController,
|
||||||
this.selectedPoll,
|
this.selectedPoll,
|
||||||
required this.onPollSelected,
|
required this.onPollSelected,
|
||||||
|
this.selectedFund,
|
||||||
|
required this.onFundSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -132,7 +138,18 @@ class _ExpandedSection extends StatelessWidget {
|
|||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(8),
|
Radius.circular(8),
|
||||||
),
|
),
|
||||||
onTap: () {},
|
onTap: () async {
|
||||||
|
final fund =
|
||||||
|
await showModalBottomSheet<SnWalletFund>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => const ComposeFundSheet(),
|
||||||
|
);
|
||||||
|
if (fund != null) {
|
||||||
|
onFundSelected(fund);
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
color:
|
color:
|
||||||
@@ -145,7 +162,7 @@ class _ExpandedSection extends StatelessWidget {
|
|||||||
Icon(Symbols.currency_exchange),
|
Icon(Symbols.currency_exchange),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(
|
Text(
|
||||||
'Fund',
|
'fund'.tr(),
|
||||||
style:
|
style:
|
||||||
Theme.of(context).textTheme.bodySmall,
|
Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
@@ -195,6 +212,8 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
final Map<String, Map<int, double?>> attachmentProgress;
|
final Map<String, Map<int, double?>> attachmentProgress;
|
||||||
final SnPoll? selectedPoll;
|
final SnPoll? selectedPoll;
|
||||||
final Function(SnPoll?) onPollSelected;
|
final Function(SnPoll?) onPollSelected;
|
||||||
|
final SnWalletFund? selectedFund;
|
||||||
|
final Function(SnWalletFund?) onFundSelected;
|
||||||
|
|
||||||
const ChatInput({
|
const ChatInput({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -217,6 +236,8 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
required this.attachmentProgress,
|
required this.attachmentProgress,
|
||||||
this.selectedPoll,
|
this.selectedPoll,
|
||||||
required this.onPollSelected,
|
required this.onPollSelected,
|
||||||
|
this.selectedFund,
|
||||||
|
required this.onFundSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -515,6 +536,114 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
key: ValueKey('no-selected-poll'),
|
key: ValueKey('no-selected-poll'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
switchInCurve: Curves.easeOutCubic,
|
||||||
|
switchOutCurve: Curves.easeInCubic,
|
||||||
|
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||||
|
return SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: const Offset(0, -0.25),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(animation),
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: animation,
|
||||||
|
axisAlignment: -1.0,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child:
|
||||||
|
selectedFund != null
|
||||||
|
? Container(
|
||||||
|
key: const ValueKey('selected-fund'),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.outline.withOpacity(0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
bottom: 8,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.currency_exchange,
|
||||||
|
size: 18,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${selectedFund!.totalAmount.toStringAsFixed(2)} ${selectedFund!.currency}',
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall!.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (selectedFund!.message != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 2),
|
||||||
|
child: Text(
|
||||||
|
selectedFund!.message!,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall!.copyWith(
|
||||||
|
fontSize: 10,
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: const Icon(Icons.close, size: 18),
|
||||||
|
onPressed: () => onFundSelected(null),
|
||||||
|
tooltip: 'clear'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(
|
||||||
|
key: ValueKey('no-selected-fund'),
|
||||||
|
),
|
||||||
|
),
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
switchInCurve: Curves.easeOutCubic,
|
switchInCurve: Curves.easeOutCubic,
|
||||||
@@ -904,6 +1033,8 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
messageController: messageController,
|
messageController: messageController,
|
||||||
selectedPoll: selectedPoll,
|
selectedPoll: selectedPoll,
|
||||||
onPollSelected: onPollSelected,
|
onPollSelected: onPollSelected,
|
||||||
|
selectedFund: selectedFund,
|
||||||
|
onFundSelected: onFundSelected,
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(key: ValueKey('collapsed')),
|
: const SizedBox.shrink(key: ValueKey('collapsed')),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -144,7 +144,11 @@ class ChatLinkAttachment extends HookConsumerWidget {
|
|||||||
helperText: 'fileIdHint'.tr(),
|
helperText: 'fileIdHint'.tr(),
|
||||||
helperMaxLines: 3,
|
helperMaxLines: 3,
|
||||||
errorText: errorMessage.value,
|
errorText: errorMessage.value,
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) =>
|
(_) =>
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ Future<void> _showSetTokenDialog(BuildContext context, WidgetRef ref) async {
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: 'Enter access token',
|
hintText: 'Enter access token',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
),
|
),
|
||||||
@@ -96,7 +98,7 @@ class DebugSheet extends HookConsumerWidget {
|
|||||||
'Unable to check for updates',
|
'Unable to check for updates',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 8),
|
const Divider(height: 8),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
@@ -455,7 +455,11 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
return TextField(
|
return TextField(
|
||||||
controller: _textController,
|
controller: _textController,
|
||||||
maxLines: 6,
|
maxLines: 6,
|
||||||
decoration: const InputDecoration(border: OutlineInputBorder()),
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
388
lib/widgets/post/compose_fund.dart
Normal file
388
lib/widgets/post/compose_fund.dart
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/wallet.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/screens/wallet.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
import 'package:island/widgets/payment/payment_overlay.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
/// Bottom sheet for selecting or creating a fund. Returns SnWalletFund via Navigator.pop.
|
||||||
|
class ComposeFundSheet extends HookConsumerWidget {
|
||||||
|
const ComposeFundSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isPushing = useState(false);
|
||||||
|
final errorText = useState<String?>(null);
|
||||||
|
|
||||||
|
return SheetScaffold(
|
||||||
|
heightFactor: 0.6,
|
||||||
|
titleText: 'fund'.tr(),
|
||||||
|
child: DefaultTabController(
|
||||||
|
length: 2,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
TabBar(
|
||||||
|
tabs: [
|
||||||
|
Tab(text: 'fundsRecent'.tr()),
|
||||||
|
Tab(text: 'fundCreateNew'.tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
children: [
|
||||||
|
// Link/Select existing fund list
|
||||||
|
ref
|
||||||
|
.watch(walletFundsProvider())
|
||||||
|
.when(
|
||||||
|
data:
|
||||||
|
(funds) =>
|
||||||
|
funds.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.money_bag,
|
||||||
|
size: 48,
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.outline,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Text(
|
||||||
|
'noFundsCreated'.tr(),
|
||||||
|
style:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: funds.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final fund = funds[index];
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
bottom: 8,
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap:
|
||||||
|
() => Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(fund),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Symbols.money_bag,
|
||||||
|
color:
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
fill: 1,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${fund.totalAmount.toStringAsFixed(2)} ${fund.currency}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight:
|
||||||
|
FontWeight.bold,
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
_getFundStatusColor(
|
||||||
|
context,
|
||||||
|
fund.status,
|
||||||
|
).withOpacity(
|
||||||
|
0.1,
|
||||||
|
),
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(
|
||||||
|
12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_getFundStatusText(
|
||||||
|
fund.status,
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
_getFundStatusColor(
|
||||||
|
context,
|
||||||
|
fund.status,
|
||||||
|
),
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight:
|
||||||
|
FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (fund.message != null &&
|
||||||
|
fund
|
||||||
|
.message!
|
||||||
|
.isNotEmpty) ...[
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
fund.message!,
|
||||||
|
style:
|
||||||
|
Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'${'recipients'.tr()}: ${fund.recipients.where((r) => r.isReceived).length}/${fund.recipients.length}',
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(
|
||||||
|
color:
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
loading:
|
||||||
|
() => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
error:
|
||||||
|
(error, stack) =>
|
||||||
|
Center(child: Text('Error: $error')),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Create new fund and return it
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'fundCreateNewHint',
|
||||||
|
).tr().fontSize(13).opacity(0.85).padding(bottom: 8),
|
||||||
|
if (errorText.value != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 4,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
errorText.value!,
|
||||||
|
style: TextStyle(color: Colors.red[700]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: FilledButton.icon(
|
||||||
|
icon:
|
||||||
|
isPushing.value
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Symbols.add_circle),
|
||||||
|
label: Text('create'.tr()),
|
||||||
|
onPressed:
|
||||||
|
isPushing.value
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
errorText.value = null;
|
||||||
|
|
||||||
|
isPushing.value = true;
|
||||||
|
// Show modal bottom sheet with fund creation form and await result
|
||||||
|
final result = await showModalBottomSheet<
|
||||||
|
Map<String, dynamic>
|
||||||
|
>(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) =>
|
||||||
|
const CreateFundSheet(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
isPushing.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
final client = ref.read(
|
||||||
|
apiClientProvider,
|
||||||
|
);
|
||||||
|
showLoadingModal(context);
|
||||||
|
|
||||||
|
final resp = await client.post(
|
||||||
|
'/pass/wallets/funds',
|
||||||
|
data: result,
|
||||||
|
options: Options(
|
||||||
|
headers: {'X-Noop': true},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final fund = SnWalletFund.fromJson(
|
||||||
|
resp.data,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fund.status == 0) {
|
||||||
|
// Return the fund that was just created (but not yet paid)
|
||||||
|
if (context.mounted) {
|
||||||
|
hideLoadingModal(context);
|
||||||
|
}
|
||||||
|
Navigator.of(context).pop(fund);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final orderResp = await client.post(
|
||||||
|
'/pass/wallets/funds/${fund.id}/order',
|
||||||
|
);
|
||||||
|
final order = SnWalletOrder.fromJson(
|
||||||
|
orderResp.data,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
hideLoadingModal(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show payment overlay to complete the payment
|
||||||
|
if (!context.mounted) return;
|
||||||
|
final paidOrder =
|
||||||
|
await PaymentOverlay.show(
|
||||||
|
context: context,
|
||||||
|
order: order,
|
||||||
|
enableBiometric: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (paidOrder != null &&
|
||||||
|
context.mounted) {
|
||||||
|
showLoadingModal(context);
|
||||||
|
|
||||||
|
// Wait for server to handle order
|
||||||
|
await Future.delayed(
|
||||||
|
const Duration(seconds: 1),
|
||||||
|
);
|
||||||
|
ref.invalidate(walletFundsProvider);
|
||||||
|
|
||||||
|
// Return the created fund
|
||||||
|
final updatedResp = await client.get(
|
||||||
|
'/pass/wallets/funds/${fund.id}',
|
||||||
|
);
|
||||||
|
final updatedFund =
|
||||||
|
SnWalletFund.fromJson(
|
||||||
|
updatedResp.data,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
hideLoadingModal(context);
|
||||||
|
}
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(updatedFund);
|
||||||
|
} else {
|
||||||
|
isPushing.value = false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (context.mounted) {
|
||||||
|
hideLoadingModal(context);
|
||||||
|
}
|
||||||
|
errorText.value = err.toString();
|
||||||
|
isPushing.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24, vertical: 24),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getFundStatusText(int status) {
|
||||||
|
switch (status) {
|
||||||
|
case 0:
|
||||||
|
return 'fundStatusCreated'.tr();
|
||||||
|
case 1:
|
||||||
|
return 'fundStatusPartial'.tr();
|
||||||
|
case 2:
|
||||||
|
return 'fundStatusCompleted'.tr();
|
||||||
|
case 3:
|
||||||
|
return 'fundStatusExpired'.tr();
|
||||||
|
default:
|
||||||
|
return 'fundStatusUnknown'.tr();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getFundStatusColor(BuildContext context, int status) {
|
||||||
|
switch (status) {
|
||||||
|
case 0:
|
||||||
|
return Colors.blue;
|
||||||
|
case 1:
|
||||||
|
return Colors.orange;
|
||||||
|
case 2:
|
||||||
|
return Colors.green;
|
||||||
|
case 3:
|
||||||
|
return Colors.red;
|
||||||
|
default:
|
||||||
|
return Theme.of(context).colorScheme.primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -143,7 +143,11 @@ class ComposeLinkAttachment extends HookConsumerWidget {
|
|||||||
helperText: 'fileIdHint'.tr(),
|
helperText: 'fileIdHint'.tr(),
|
||||||
helperMaxLines: 3,
|
helperMaxLines: 3,
|
||||||
errorText: errorMessage.value,
|
errorText: errorMessage.value,
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTapOutside:
|
onTapOutside:
|
||||||
(_) =>
|
(_) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user