diff --git a/lib/app/layouts/chat_creator/chat_creator.dart b/lib/app/layouts/chat_creator/chat_creator.dart index bcf4cc8f0..41beffff3 100644 --- a/lib/app/layouts/chat_creator/chat_creator.dart +++ b/lib/app/layouts/chat_creator/chat_creator.dart @@ -72,7 +72,7 @@ class ChatCreatorState extends OptimizedState { final messageNode = FocusNode(); - bool canCreateGroupChats = ss.canCreateGroupChatSync(); + bool canCreateGroupChats = backend.canCreateGroupChats; @override void initState() { @@ -147,8 +147,7 @@ class ChatCreatorState extends OptimizedState { void addSelected(SelectedContact c) async { selectedContacts.add(c); try { - final response = await http.handleiMessageState(c.address); - c.iMessage.value = response.data["data"]["available"]; + c.iMessage.value = await backend.handleiMessageState(c.address); } catch (_) {} addressController.text = ""; findExistingChat(); @@ -450,59 +449,60 @@ class ChatCreatorState extends OptimizedState { ], ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 15.0).add(const EdgeInsets.only(bottom: 5.0)), - child: ToggleButtons( - constraints: BoxConstraints(minWidth: (ns.width(context) - 35) / 2), - fillColor: context.theme.colorScheme.bubble(context, iMessage).withOpacity(0.2), - splashColor: context.theme.colorScheme.bubble(context, iMessage).withOpacity(0.2), - children: [ - const Row( - children: [ - Padding( - padding: EdgeInsets.all(8.0), - child: Text("iMessage"), - ), - Icon(CupertinoIcons.chat_bubble, size: 16), - ], - ), - const Row( - children: [ - Padding( - padding: EdgeInsets.all(8.0), - child: Text("SMS Forwarding"), - ), - Icon(Icons.messenger_outline, size: 16), - ], - ), - ], - borderRadius: BorderRadius.circular(20), - selectedBorderColor: context.theme.colorScheme.bubble(context, iMessage), - selectedColor: context.theme.colorScheme.bubble(context, iMessage), - isSelected: [iMessage, sms], - onPressed: (index) async { - selectedContacts.clear(); - addressController.text = ""; - if (index == 0) { - setState(() { - iMessage = true; - sms = false; - filteredChats = List.from(existingChats.where((e) => e.isIMessage)); - }); - await cm.setAllInactive(); - fakeController.value = null; - } else { - setState(() { - iMessage = false; - sms = true; - filteredChats = List.from(existingChats.where((e) => !e.isIMessage)); - }); - await cm.setAllInactive(); - fakeController.value = null; - } - }, + if (backend.supportsSmsForwarding) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0).add(const EdgeInsets.only(bottom: 5.0)), + child: ToggleButtons( + constraints: BoxConstraints(minWidth: (ns.width(context) - 35) / 2), + fillColor: context.theme.colorScheme.bubble(context, iMessage).withOpacity(0.2), + splashColor: context.theme.colorScheme.bubble(context, iMessage).withOpacity(0.2), + children: [ + const Row( + children: [ + Padding( + padding: EdgeInsets.all(8.0), + child: Text("iMessage"), + ), + Icon(CupertinoIcons.chat_bubble, size: 16), + ], + ), + const Row( + children: [ + Padding( + padding: EdgeInsets.all(8.0), + child: Text("SMS Forwarding"), + ), + Icon(Icons.messenger_outline, size: 16), + ], + ), + ], + borderRadius: BorderRadius.circular(20), + selectedBorderColor: context.theme.colorScheme.bubble(context, iMessage), + selectedColor: context.theme.colorScheme.bubble(context, iMessage), + isSelected: [iMessage, sms], + onPressed: (index) async { + selectedContacts.clear(); + addressController.text = ""; + if (index == 0) { + setState(() { + iMessage = true; + sms = false; + filteredChats = List.from(existingChats.where((e) => e.isIMessage)); + }); + await cm.setAllInactive(); + fakeController.value = null; + } else { + setState(() { + iMessage = false; + sms = true; + filteredChats = List.from(existingChats.where((e) => !e.isIMessage)); + }); + await cm.setAllInactive(); + fakeController.value = null; + } + }, + ), ), - ), Expanded( child: Theme( data: context.theme.copyWith( @@ -668,13 +668,14 @@ class ChatCreatorState extends OptimizedState { sendMessage: ({String? effect}) async { addressOnSubmitted(); final chat = fakeController.value?.chat ?? await findExistingChat(checkDeleted: true, update: false); - bool existsOnServer = false; - if (chat != null) { + bool existsOnServer = true; // if there is no remote, we exist on the "server" + if (chat != null && backend.remoteService != null) { // if we don't error, then the chat exists try { - await http.singleChat(chat.guid); - existsOnServer = true; - } catch (_) {} + await backend.remoteService!.singleChat(chat.guid); + } catch (_) { + existsOnServer = false; + } } if (chat != null && existsOnServer) { ns.pushAndRemoveUntil( @@ -721,6 +722,7 @@ class ChatCreatorState extends OptimizedState { final method = iMessage ? "iMessage" : "SMS"; showDialog( context: context, + barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( backgroundColor: context.theme.colorScheme.properSurface, @@ -739,9 +741,8 @@ class ChatCreatorState extends OptimizedState { ), ); }); - http.createChat(participants, textController.text, method).then((response) async { + backend.createChat(participants, textController.text, method).then((newChat) async { // Load the chat data and save it to the DB - Chat newChat = Chat.fromMap(response.data["data"]); newChat = newChat.save(); // Fetch the newly saved chat data from the DB @@ -756,8 +757,8 @@ class ChatCreatorState extends OptimizedState { chats.updateChat(newChat); // Fetch the last message for the chat and save it. - final messageRes = await http.chatMessages(newChat.guid, limit: 1); - if (messageRes.data["data"].length > 0) { + final messageRes = await backend.remoteService?.chatMessages(newChat.guid, limit: 1); + if (messageRes != null && messageRes.data["data"].length > 0) { final messages = (messageRes.data["data"] as List).map((e) => Message.fromMap(e)).toList(); await Chat.bulkSyncMessages(newChat, messages); } @@ -771,7 +772,7 @@ class ChatCreatorState extends OptimizedState { createCompleter?.complete(); // Navigate to the new chat - Navigator.of(context).pop(); + Get.back(closeOverlays: true); ns.pushAndRemoveUntil( Get.context!, ConversationView(chat: newChat), @@ -787,7 +788,7 @@ class ChatCreatorState extends OptimizedState { ), ); }).catchError((error) { - Navigator.of(context).pop(); + Get.back(closeOverlays: true); showDialog( barrierDismissible: false, context: context, diff --git a/lib/app/layouts/conversation_details/conversation_details.dart b/lib/app/layouts/conversation_details/conversation_details.dart index 768e1220f..6a7769673 100644 --- a/lib/app/layouts/conversation_details/conversation_details.dart +++ b/lib/app/layouts/conversation_details/conversation_details.dart @@ -278,7 +278,7 @@ class _ConversationDetailsState extends OptimizedState with ); }, childCount: clippedParticipants.length + 2), ), - if (chat.participants.length > 2 && ss.settings.enablePrivateAPI.value && ss.serverDetailsSync().item4 >= 226) + if (chat.participants.length > 2 && ss.settings.enablePrivateAPI.value && backend.canLeaveChat) SliverToBoxAdapter( child: Builder( builder: (context) { @@ -321,8 +321,8 @@ class _ConversationDetailsState extends OptimizedState with ); } ); - final response = await http.leaveChat(chat.guid); - if (response.statusCode == 200) { + final response = await backend.leaveChat(chat); + if (response) { Get.back(); showSnackbar("Notice", "Left chat successfully!"); } else { diff --git a/lib/app/layouts/conversation_details/dialogs/add_participant.dart b/lib/app/layouts/conversation_details/dialogs/add_participant.dart index 123f02f6a..f9c806e38 100644 --- a/lib/app/layouts/conversation_details/dialogs/add_participant.dart +++ b/lib/app/layouts/conversation_details/dialogs/add_participant.dart @@ -127,8 +127,8 @@ void showAddParticipant(BuildContext context, Chat chat) { ); } ); - final response = await http.chatParticipant("add", chat.guid, participantController.text); - if (response.statusCode == 200) { + final response = await backend.chatParticipant(ParticipantOp.Add, chat, participantController.text); + if (response) { Get.back(); Get.back(); showSnackbar("Notice", "Added ${participantController.text} successfully!"); diff --git a/lib/app/layouts/conversation_details/dialogs/change_name.dart b/lib/app/layouts/conversation_details/dialogs/change_name.dart index f208fec7b..bdf520e4f 100644 --- a/lib/app/layouts/conversation_details/dialogs/change_name.dart +++ b/lib/app/layouts/conversation_details/dialogs/change_name.dart @@ -39,8 +39,8 @@ void showChangeName(Chat chat, String method, BuildContext context) { ); } ); - final response = await http.updateChat(chat.guid, controller.text); - if (response.statusCode == 200) { + final response = await backend.renameChat(chat, controller.text); + if (response) { Get.back(); Get.back(); chat.changeName(controller.text); diff --git a/lib/app/layouts/conversation_details/widgets/chat_info.dart b/lib/app/layouts/conversation_details/widgets/chat_info.dart index 24af05d78..8a5bf0c33 100644 --- a/lib/app/layouts/conversation_details/widgets/chat_info.dart +++ b/lib/app/layouts/conversation_details/widgets/chat_info.dart @@ -44,7 +44,7 @@ class _ChatInfoState extends OptimizedState { children: [ if (ss.settings.enablePrivateAPI.value && chat.isIMessage) Text( - "Local - Changes only apply to this device.\nPrivate API - Changes will apply to everyone's devices.", + "Local - Changes only apply to this device.\nEveryone - Changes will apply to everyone's devices.", style: context.theme.textTheme.bodyLarge ), ], @@ -61,7 +61,7 @@ class _ChatInfoState extends OptimizedState { ), TextButton( child: Text( - "Private API", + "Everyone", style: context.theme.textTheme.bodyLarge!.copyWith(color: context.theme.colorScheme.primary) ), onPressed: () { @@ -88,7 +88,7 @@ class _ChatInfoState extends OptimizedState { if (result != null) { chat.customAvatarPath = result; } - if (papi && ss.settings.enablePrivateAPI.value && result != null && (await ss.isMinBigSur) && ss.serverDetailsSync().item4 >= 226) { + if (papi && ss.settings.enablePrivateAPI.value && result != null && await backend.canUploadGroupPhotos()) { showDialog( context: context, builder: (BuildContext context) { @@ -110,8 +110,8 @@ class _ChatInfoState extends OptimizedState { ); } ); - final response = await http.setChatIcon(chat.guid, chat.customAvatarPath!); - if (response.statusCode == 200) { + final response = await backend.setChatIcon(chat); + if (response) { Get.back(); showSnackbar("Notice", "Updated group photo successfully!"); } else { @@ -127,15 +127,11 @@ class _ChatInfoState extends OptimizedState { papi = await showMethodDialog("Group Icon Deletion Method"); } if (papi == null) return; - try { - File file = File(chat.customAvatarPath!); - file.delete(); - } catch (_) {} - chat.customAvatarPath = null; + chat.removeProfilePhoto(); chat.save(updateCustomAvatarPath: true); - if (papi && ss.settings.enablePrivateAPI.value && (await ss.isMinBigSur) && ss.serverDetailsSync().item4 >= 226) { - final response = await http.deleteChatIcon(chat.guid); - if (response.statusCode == 200) { + if (papi && ss.settings.enablePrivateAPI.value && await backend.canUploadGroupPhotos()) { + final response = await backend.deleteChatIcon(chat); + if (response) { showSnackbar("Notice", "Deleted group photo successfully!"); } else { showSnackbar("Error", "Failed to delete group photo!"); @@ -328,7 +324,7 @@ class _ChatInfoState extends OptimizedState { child: Row( mainAxisAlignment: kIsWeb || kIsDesktop ? MainAxisAlignment.center : MainAxisAlignment.spaceBetween, children: intersperse(const SizedBox(width: 5), [ - if (!kIsWeb && !kIsDesktop && !chat.chatIdentifier!.startsWith("urn:biz") + if (!kIsWeb && !kIsDesktop && !(chat.chatIdentifier?.startsWith("urn:biz") ?? false) && ((chat.participants.first.contact?.phones.isNotEmpty ?? false) || !chat.participants.first.address.contains("@"))) Expanded( diff --git a/lib/app/layouts/conversation_details/widgets/chat_options.dart b/lib/app/layouts/conversation_details/widgets/chat_options.dart index 326751f93..d75c5e02e 100644 --- a/lib/app/layouts/conversation_details/widgets/chat_options.dart +++ b/lib/app/layouts/conversation_details/widgets/chat_options.dart @@ -141,55 +141,58 @@ class _ChatOptionsState extends OptimizedState { onTap: () async { showBookmarksThread(cvc(widget.chat), context); }), - SettingsTile( - title: "Fetch Chat Details", - subtitle: "Get the latest chat title and participants from the server", - backgroundColor: tileColor, + if (backend.remoteService != null) + SettingsTile( + title: "Fetch Chat Details", + subtitle: "Get the latest chat title and participants from the server", + backgroundColor: tileColor, + trailing: Padding( + padding: const EdgeInsets.only(right: 15.0), + child: Icon(iOS ? CupertinoIcons.chat_bubble : Icons.sms), + ), + onTap: () async { + await cm.fetchChat(chat.guid); + showSnackbar("Notice", "Fetched details!"); + }), + if (backend.remoteService != null) + SettingsTile( + title: "Fetch More Messages", + subtitle: "Fetches up to 100 messages after the last message stored locally", + isThreeLine: true, trailing: Padding( padding: const EdgeInsets.only(right: 15.0), - child: Icon(iOS ? CupertinoIcons.chat_bubble : Icons.sms), + child: Icon(iOS ? CupertinoIcons.cloud_download : Icons.file_download), ), onTap: () async { - await cm.fetchChat(chat.guid); - showSnackbar("Notice", "Fetched details!"); - }), - SettingsTile( - title: "Fetch More Messages", - subtitle: "Fetches up to 100 messages after the last message stored locally", - isThreeLine: true, - trailing: Padding( - padding: const EdgeInsets.only(right: 15.0), - child: Icon(iOS ? CupertinoIcons.cloud_download : Icons.file_download), + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => ChatSyncDialog( + chat: chat, + withOffset: true, + initialMessage: "Fetching more messages...", + limit: 100, + ), + ); + }, ), - onTap: () async { - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) => ChatSyncDialog( - chat: chat, - withOffset: true, - initialMessage: "Fetching more messages...", - limit: 100, - ), - ); - }, - ), - SettingsTile( - title: "Sync Last 25 Messages", - subtitle: "Resyncs the 25 most recent messages from the server", - trailing: Padding( - padding: const EdgeInsets.only(right: 15.0), - child: Icon(iOS ? CupertinoIcons.arrow_counterclockwise : Icons.replay), + if (backend.remoteService != null) + SettingsTile( + title: "Sync Last 25 Messages", + subtitle: "Resyncs the 25 most recent messages from the server", + trailing: Padding( + padding: const EdgeInsets.only(right: 15.0), + child: Icon(iOS ? CupertinoIcons.arrow_counterclockwise : Icons.replay), + ), + onTap: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => + ChatSyncDialog(chat: chat, initialMessage: "Resyncing messages...", limit: 25), + ); + }, ), - onTap: () { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => - ChatSyncDialog(chat: chat, initialMessage: "Resyncing messages...", limit: 25), - ); - }, - ), if (!kIsWeb && !chat.isGroup && ss.settings.enablePrivateAPI.value) const SettingsDivider(), if (!kIsWeb && !chat.isGroup && ss.settings.enablePrivateAPI.value) SettingsSwitch( diff --git a/lib/app/layouts/conversation_details/widgets/contact_tile.dart b/lib/app/layouts/conversation_details/widgets/contact_tile.dart index cc5568d9c..ef03fabcf 100644 --- a/lib/app/layouts/conversation_details/widgets/contact_tile.dart +++ b/lib/app/layouts/conversation_details/widgets/contact_tile.dart @@ -170,7 +170,7 @@ class ContactTile extends StatelessWidget { } ); - http.chatParticipant("remove", chat.guid, handle.address).then((response) async { + backend.chatParticipant(ParticipantOp.Remove, chat, handle.address).then((response) async { Get.back(); Logger.info("Removed participant ${handle.address}"); showSnackbar("Notice", "Removed participant from chat!"); diff --git a/lib/app/layouts/conversation_list/pages/search/search_view.dart b/lib/app/layouts/conversation_list/pages/search/search_view.dart index 75a6f5450..20cf216d9 100644 --- a/lib/app/layouts/conversation_list/pages/search/search_view.dart +++ b/lib/app/layouts/conversation_list/pages/search/search_view.dart @@ -50,7 +50,7 @@ class SearchViewState extends OptimizedState { bool noResults = false; bool isSearching = false; String? currentSearchTerm; - bool local = false; + bool local = backend.remoteService == null; bool network = true; Color get backgroundColor => ss.settings.windowEffect.value == WindowEffect.disabled @@ -94,7 +94,7 @@ class SearchViewState extends OptimizedState { ); if (local) { - final query = (messageBox.query(Message_.text.contains(currentSearchTerm!) + final query = (messageBox.query(Message_.text.contains(currentSearchTerm!, caseSensitive: false) .and(Message_.associatedMessageGuid.isNull()) .and(Message_.dateDeleted.isNull()) .and(Message_.dateCreated.notNull())) @@ -253,7 +253,7 @@ class SearchViewState extends OptimizedState { suffixMode: OverlayVisibilityMode.editing, ), ), - if (!kIsWeb) + if (!kIsWeb && backend.remoteService != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), child: ToggleButtons( diff --git a/lib/app/layouts/conversation_list/widgets/header/header_widgets.dart b/lib/app/layouts/conversation_list/widgets/header/header_widgets.dart index 9325ae849..65b44bfd7 100644 --- a/lib/app/layouts/conversation_list/widgets/header/header_widgets.dart +++ b/lib/app/layouts/conversation_list/widgets/header/header_widgets.dart @@ -214,7 +214,7 @@ class OverflowMenu extends StatelessWidget { style: context.textTheme.bodyLarge!.apply(color: context.theme.colorScheme.properOnSurface), ), ), - if (ss.isMinCatalinaSync) + if (backend.supportsFindMy) PopupMenuItem( value: 5, child: Text( diff --git a/lib/app/layouts/conversation_view/pages/messages_view.dart b/lib/app/layouts/conversation_view/pages/messages_view.dart index 9e7e46d4f..b571dff9a 100644 --- a/lib/app/layouts/conversation_view/pages/messages_view.dart +++ b/lib/app/layouts/conversation_view/pages/messages_view.dart @@ -140,7 +140,7 @@ class MessagesViewState extends OptimizedState { } void getFocusState() { - if (!ss.isMinMontereySync) return; + if (!backend.supportsFocusStates()) return; final recipient = chat.participants.firstOrNull; if (recipient != null) { http.handleFocusState(recipient.address).then((response) { diff --git a/lib/app/layouts/conversation_view/widgets/header/header_widgets.dart b/lib/app/layouts/conversation_view/widgets/header/header_widgets.dart index 3d7936b22..0cc13386f 100644 --- a/lib/app/layouts/conversation_view/widgets/header/header_widgets.dart +++ b/lib/app/layouts/conversation_view/widgets/header/header_widgets.dart @@ -64,9 +64,9 @@ class ManualMarkState extends OptimizedState { marking = true; }); if (!marked) { - await http.markChatRead(chat.guid); + await backend.markRead(chat, ss.settings.privateMarkChatAsRead.value); } else { - await http.markChatUnread(chat.guid); + await backend.markUnread(chat); } setState(() { marking = false; diff --git a/lib/app/layouts/conversation_view/widgets/message/attachment/attachment_holder.dart b/lib/app/layouts/conversation_view/widgets/message/attachment/attachment_holder.dart index 66b207cf0..05685f789 100644 --- a/lib/app/layouts/conversation_view/widgets/message/attachment/attachment_holder.dart +++ b/lib/app/layouts/conversation_view/widgets/message/attachment/attachment_holder.dart @@ -160,7 +160,7 @@ class _AttachmentHolderState extends CustomState element.value == message.expressiveSendStyleId)?.key ?? "unknown"; + playEffect() { + if (stringToMessageEffect[effect] == MessageEffect.echo) { + showSnackbar("Notice", "Echo animation is not supported at this time."); + return; + } + HapticFeedback.mediumImpact(); + if ((stringToMessageEffect[effect] ?? MessageEffect.none).isBubble) { + eventDispatcher.emit('play-bubble-effect', '${widget.part.part}/${message.guid}'); + } else if (widget.globalKey != null) { + eventDispatcher.emit('play-effect', { + 'type': effect, + 'size': widget.globalKey!.globalPaintBounds(context), + }); + } + } + if (message.datePlayed == null && !(message.isFromMe ?? false)) { + message.datePlayed = DateTime.now(); + message.save(); + var needsAlignment = stringToMessageEffect[effect] == MessageEffect.spotlight || + stringToMessageEffect[effect] == MessageEffect.love || stringToMessageEffect[effect] == MessageEffect.lasers; + // wait extra long for effects that need the message to be in it's final position + Future.delayed(Duration(milliseconds: needsAlignment ? 500 : 200), () { + playEffect(); + }); + + } properties.add(TextSpan( text: "↺ sent with $effect", recognizer: TapGestureRecognizer()..onTap = () { - if (stringToMessageEffect[effect] == MessageEffect.echo) { - showSnackbar("Notice", "Echo animation is not supported at this time."); - return; - } - HapticFeedback.mediumImpact(); - if ((stringToMessageEffect[effect] ?? MessageEffect.none).isBubble) { - eventDispatcher.emit('play-bubble-effect', '${widget.part.part}/${message.guid}'); - } else if (widget.globalKey != null) { - eventDispatcher.emit('play-effect', { - 'type': effect, - 'size': widget.globalKey!.globalPaintBounds(context), - }); - } + playEffect(); } )); } diff --git a/lib/app/layouts/conversation_view/widgets/message/popup/message_popup.dart b/lib/app/layouts/conversation_view/widgets/message/popup/message_popup.dart index 943b0f6cc..bb8f3ed6f 100644 --- a/lib/app/layouts/conversation_view/widgets/message/popup/message_popup.dart +++ b/lib/app/layouts/conversation_view/widgets/message/popup/message_popup.dart @@ -349,10 +349,9 @@ class _MessagePopupState extends OptimizedState with SingleTickerP style: context.textTheme.bodyLarge!.apply(color: context.theme.colorScheme.properOnSurface), ), ), - if (ss.isMinVenturaSync && + if (backend.canEditUnsend && message.isFromMe! && - !message.guid!.startsWith("temp") && - ss.serverDetailsSync().item4 >= 148) + !message.guid!.startsWith("temp")) PopupMenuItem( value: 6, child: Text( @@ -360,10 +359,9 @@ class _MessagePopupState extends OptimizedState with SingleTickerP style: context.textTheme.bodyLarge!.apply(color: context.theme.colorScheme.properOnSurface), ), ), - if (ss.isMinVenturaSync && + if (backend.canEditUnsend && message.isFromMe! && !message.guid!.startsWith("temp") && - ss.serverDetailsSync().item4 >= 148 && (part.text?.isNotEmpty ?? false)) PopupMenuItem( value: 7, @@ -771,13 +769,8 @@ class _MessagePopupState extends OptimizedState with SingleTickerP try { for (Attachment? element in toDownload) { attachmentObs.value = element; - final response = await http.downloadAttachment(element!.guid!, + final file = await backend.downloadAttachment(element!, original: true, onReceiveProgress: (count, total) => progress.value = kIsWeb ? (count / total) : (count / element.totalBytes!)); - final file = PlatformFile( - name: element.transferName!, - size: response.data.length, - bytes: response.data, - ); await as.saveToDisk(file); } progress.value = 1; @@ -848,15 +841,9 @@ class _MessagePopupState extends OptimizedState with SingleTickerP try { for (Attachment? element in toDownload) { attachmentObs.value = element; - final response = await http.downloadLivePhoto(element!.guid!, + final nameSplit = element!.transferName!.split("."); + await backend.downloadLivePhoto(element, "${nameSplit.take(nameSplit.length - 1).join(".")}.mov", onReceiveProgress: (count, total) => progress.value = kIsWeb ? (count / total) : (count / element.totalBytes!)); - final nameSplit = element.transferName!.split("."); - final file = PlatformFile( - name: "${nameSplit.take(nameSplit.length - 1).join(".")}.mov", - size: response.data.length, - bytes: response.data, - ); - await as.saveToDisk(file); } progress.value = 1; downloadingAttachments.value = false; @@ -981,9 +968,8 @@ class _MessagePopupState extends OptimizedState with SingleTickerP void unsend() async { popDetails(); - final response = await http.unsend(message.guid!, partIndex: part.part); - if (response.statusCode == 200) { - final updatedMessage = Message.fromMap(response.data['data']); + final updatedMessage = await backend.unsend(message, part); + if (updatedMessage != null) { ah.handleUpdatedMessage(chat, updatedMessage, null); } } @@ -1327,7 +1313,7 @@ class _MessagePopupState extends OptimizedState with SingleTickerP ), ), ), - if (ss.isMinVenturaSync && message.isFromMe! && !message.guid!.startsWith("temp") && ss.serverDetailsSync().item4 >= 148) + if (backend.canEditUnsend && message.isFromMe! && !message.guid!.startsWith("temp")) Material( color: Colors.transparent, child: InkWell( @@ -1346,10 +1332,9 @@ class _MessagePopupState extends OptimizedState with SingleTickerP ), ), ), - if (ss.isMinVenturaSync && + if (backend.canEditUnsend && message.isFromMe! && !message.guid!.startsWith("temp") && - ss.serverDetailsSync().item4 >= 148 && (part.text?.isNotEmpty ?? false)) Material( color: Colors.transparent, diff --git a/lib/app/layouts/conversation_view/widgets/text_field/conversation_text_field.dart b/lib/app/layouts/conversation_view/widgets/text_field/conversation_text_field.dart index 7f20a895a..bf4aadb0b 100644 --- a/lib/app/layouts/conversation_view/widgets/text_field/conversation_text_field.dart +++ b/lib/app/layouts/conversation_view/widgets/text_field/conversation_text_field.dart @@ -284,10 +284,10 @@ class ConversationTextFieldState extends CustomState { SettingsSection( backgroundColor: tileColor, children: [ - Obx(() => SettingsSwitch( - onChanged: (bool val) { - ss.settings.showConnectionIndicator.value = val; - saveSettings(); - }, - initialVal: ss.settings.showConnectionIndicator.value, - title: "Show Connection Indicator", - subtitle: "Show a visual status indicator when the app is not connected to the server", - backgroundColor: tileColor, - )), - Container( - color: tileColor, - child: Padding( - padding: const EdgeInsets.only(left: 15.0), - child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + if (backend.remoteService != null) + Obx(() => SettingsSwitch( + onChanged: (bool val) { + ss.settings.showConnectionIndicator.value = val; + saveSettings(); + }, + initialVal: ss.settings.showConnectionIndicator.value, + title: "Show Connection Indicator", + subtitle: "Show a visual status indicator when the app is not connected to the server", + backgroundColor: tileColor, + )), + if (backend.remoteService != null) + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 15.0), + child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + ), ), - ), - Obx(() => SettingsSwitch( - onChanged: (bool val) { - ss.settings.showSyncIndicator.value = val; - saveSettings(); - }, - initialVal: ss.settings.showSyncIndicator.value, - title: "Show Sync Indicator in Chat List", - subtitle: - "Enables a small indicator at the top left to show when the app is syncing messages", - backgroundColor: tileColor, - isThreeLine: true, - )), - Container( - color: tileColor, - child: Padding( - padding: const EdgeInsets.only(left: 15.0), - child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + if (backend.remoteService != null) + Obx(() => SettingsSwitch( + onChanged: (bool val) { + ss.settings.showSyncIndicator.value = val; + saveSettings(); + }, + initialVal: ss.settings.showSyncIndicator.value, + title: "Show Sync Indicator in Chat List", + subtitle: + "Enables a small indicator at the top left to show when the app is syncing messages", + backgroundColor: tileColor, + isThreeLine: true, + )), + if (backend.remoteService != null) + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 15.0), + child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + ), ), - ), Obx(() => SettingsSwitch( onChanged: (bool val) { ss.settings.statusIndicatorsOnChats.value = val; diff --git a/lib/app/layouts/settings/pages/message_view/conversation_panel.dart b/lib/app/layouts/settings/pages/message_view/conversation_panel.dart index f260e0125..6ccba9dfc 100644 --- a/lib/app/layouts/settings/pages/message_view/conversation_panel.dart +++ b/lib/app/layouts/settings/pages/message_view/conversation_panel.dart @@ -1,7 +1,10 @@ +import 'package:animated_size_and_fade/animated_size_and_fade.dart'; import 'package:audio_waveforms/audio_waveforms.dart' as aw; +import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/reaction/reaction.dart'; import 'package:bluebubbles/helpers/helpers.dart'; import 'package:bluebubbles/app/layouts/settings/widgets/settings_widgets.dart'; import 'package:bluebubbles/app/wrappers/stateful_boilerplate.dart'; +import 'package:bluebubbles/main.dart'; import 'package:bluebubbles/models/models.dart' hide PlatformFile; import 'package:bluebubbles/services/services.dart'; import 'package:file_picker/file_picker.dart'; @@ -152,7 +155,7 @@ class _ConversationPanelState extends OptimizedState { child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), ), ), - if (!kIsWeb) + if (!kIsWeb && backend.remoteService != null) SettingsTile( title: "Sync Group Chat Icons", trailing: Obx(() => gettingIcons.value == null @@ -177,7 +180,7 @@ class _ConversationPanelState extends OptimizedState { }, subtitle: "Get iMessage group chat icons from the server", ), - if (!kIsWeb) + if (!kIsWeb && backend.remoteService != null) const SettingsSubtitle( subtitle: "Note: Overrides any custom avatars set for group chats.", ), @@ -521,6 +524,212 @@ class _ConversationPanelState extends OptimizedState { )), ], ), + Obx( + () => AnimatedSizeAndFade.showHide( + show: ss.settings.enablePrivateAPI.value, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + SettingsHeader(iosSubtitle: iosSubtitle, materialSubtitle: materialSubtitle, text: usingRustPush ? "Interaction settings" : "Private API Settings"), + SettingsSection( + backgroundColor: tileColor, + children: [ + SettingsSwitch( + onChanged: (bool val) { + ss.settings.privateSendTypingIndicators.value = val; + saveSettings(); + }, + initialVal: ss.settings.privateSendTypingIndicators.value, + title: "Send Typing Indicators", + subtitle: "Sends typing indicators to other iMessage users", + backgroundColor: tileColor, + ), + AnimatedSizeAndFade( + child: !ss.settings.privateManualMarkAsRead.value + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 15.0), + child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + ), + ), + SettingsSwitch( + onChanged: (bool val) { + ss.settings.privateMarkChatAsRead.value = val; + if (val) { + ss.settings.privateManualMarkAsRead.value = false; + } + saveSettings(); + }, + initialVal: ss.settings.privateMarkChatAsRead.value, + title: "Automatic Mark Read / Send Read Receipts", + subtitle: + "Marks chats read in the iMessage app on your server and sends read receipts to other iMessage users", + backgroundColor: tileColor, + isThreeLine: true, + ), + ], + ) + : const SizedBox.shrink(), + ), + AnimatedSizeAndFade.showHide( + show: !ss.settings.privateMarkChatAsRead.value, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 15.0), + child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + ), + ), + SettingsSwitch( + onChanged: (bool val) { + ss.settings.privateManualMarkAsRead.value = val; + saveSettings(); + }, + initialVal: ss.settings.privateManualMarkAsRead.value, + title: "Manual Mark Read / Send Read Receipts", + subtitle: "Only mark a chat read when pressing the manual mark read button", + backgroundColor: tileColor, + isThreeLine: true, + ), + ], + ), + ), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 15.0), + child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + ), + ), + SettingsSwitch( + title: "Double-${kIsWeb || kIsDesktop ? "Click" : "Tap"} Message for Quick Tapback", + initialVal: ss.settings.enableQuickTapback.value, + onChanged: (bool val) { + ss.settings.enableQuickTapback.value = val; + if (val && ss.settings.doubleTapForDetails.value) { + ss.settings.doubleTapForDetails.value = false; + } + saveSettings(); + }, + subtitle: "Send a tapback of your choosing when double ${kIsWeb || kIsDesktop ? "click" : "tapp"}ing a message", + backgroundColor: tileColor, + isThreeLine: true, + ), + AnimatedSizeAndFade.showHide( + show: ss.settings.enableQuickTapback.value, + child: Padding( + padding: const EdgeInsets.only(bottom: 5.0), + child: SettingsOptions( + title: "Quick Tapback", + options: ReactionTypes.toList(), + cupertinoCustomWidgets: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 7.5), + child: ReactionWidget( + reaction: Message( + guid: "", + associatedMessageType: ReactionTypes.LOVE, + isFromMe: ss.settings.quickTapbackType.value != ReactionTypes.LOVE), + message: null, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 7.5), + child: ReactionWidget( + reaction: Message( + guid: "", + associatedMessageType: ReactionTypes.LIKE, + isFromMe: ss.settings.quickTapbackType.value != ReactionTypes.LIKE), + message: null, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 7.5), + child: ReactionWidget( + reaction: Message( + guid: "", + associatedMessageType: ReactionTypes.DISLIKE, + isFromMe: ss.settings.quickTapbackType.value != ReactionTypes.DISLIKE), + message: null, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 7.5), + child: ReactionWidget( + reaction: Message( + guid: "", + associatedMessageType: ReactionTypes.LAUGH, + isFromMe: ss.settings.quickTapbackType.value != ReactionTypes.LAUGH), + message: null, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 7.5), + child: ReactionWidget( + reaction: Message( + guid: "", + associatedMessageType: ReactionTypes.EMPHASIZE, + isFromMe: ss.settings.quickTapbackType.value != ReactionTypes.EMPHASIZE), + message: null, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 7.5), + child: ReactionWidget( + reaction: Message( + guid: "", + associatedMessageType: ReactionTypes.QUESTION, + isFromMe: ss.settings.quickTapbackType.value != ReactionTypes.QUESTION), + message: null, + ), + ), + ], + initial: ss.settings.quickTapbackType.value, + textProcessing: (val) => val, + onChanged: (val) { + if (val == null) return; + ss.settings.quickTapbackType.value = val; + saveSettings(); + }, + secondaryColor: headerColor, + ), + ), + ), + AnimatedSizeAndFade.showHide( + show: backend.canEditUnsend, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 15.0), + child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + ), + ), + SettingsSwitch( + title: "Up Arrow for Quick Edit", + initialVal: ss.settings.editLastSentMessageOnUpArrow.value, + onChanged: (bool val) { + ss.settings.editLastSentMessageOnUpArrow.value = val; + saveSettings(); + }, + subtitle: "Press the Up Arrow to begin editing the last message you sent", + backgroundColor: tileColor, + ), + ], + ), + ), + ], + ) + ]), + ), + ), ], ), ), diff --git a/lib/app/layouts/settings/pages/misc/misc_panel.dart b/lib/app/layouts/settings/pages/misc/misc_panel.dart index ce44f2e00..4982e627f 100644 --- a/lib/app/layouts/settings/pages/misc/misc_panel.dart +++ b/lib/app/layouts/settings/pages/misc/misc_panel.dart @@ -1,3 +1,4 @@ +import 'package:bluebubbles/main.dart'; import 'package:bluebubbles/services/services.dart'; import 'package:bluebubbles/helpers/helpers.dart'; import 'package:bluebubbles/app/layouts/settings/widgets/settings_widgets.dart'; @@ -250,10 +251,12 @@ class _MiscPanelState extends OptimizedState { }), ], ), + if (backend.remoteService != null) SettingsHeader( iosSubtitle: iosSubtitle, materialSubtitle: materialSubtitle, text: "Networking"), + if (backend.remoteService != null) SettingsSection( backgroundColor: tileColor, children: [ diff --git a/lib/app/layouts/settings/pages/misc/troubleshoot_panel.dart b/lib/app/layouts/settings/pages/misc/troubleshoot_panel.dart index e8173d43e..1e5200ef8 100644 --- a/lib/app/layouts/settings/pages/misc/troubleshoot_panel.dart +++ b/lib/app/layouts/settings/pages/misc/troubleshoot_panel.dart @@ -313,13 +313,13 @@ class _TroubleshootPanelState extends OptimizedState { ); }), ]), - if (!kIsWeb) + if (!kIsWeb && backend.remoteService != null) SettingsHeader( iosSubtitle: iosSubtitle, materialSubtitle: materialSubtitle, text: "Database Re-syncing" ), - if (!kIsWeb) + if (!kIsWeb && backend.remoteService != null) SettingsSection( backgroundColor: tileColor, children: [ diff --git a/lib/app/layouts/settings/pages/profile/profile_panel.dart b/lib/app/layouts/settings/pages/profile/profile_panel.dart index 1f1bfd7a8..a08767175 100644 --- a/lib/app/layouts/settings/pages/profile/profile_panel.dart +++ b/lib/app/layouts/settings/pages/profile/profile_panel.dart @@ -35,17 +35,11 @@ class _ProfilePanelState extends OptimizedState with WidgetsBindin void getDetails() async { try { - final result = await http.getAccountInfo(); - if (!isNullOrEmpty(result.data.isNotEmpty)!) { - accountInfo.addAll(result.data['data']); - } + final result = await backend.getAccountInfo(); + accountInfo.addAll(result); opacity.value = 1.0; - if (ss.isMinBigSurSync) { - final result2 = await http.getAccountContact(); - if (!isNullOrEmpty(result2.data.isNotEmpty)!) { - accountContact.addAll(result2.data['data']); - } - } + final result2 = await backend.getAccountContact(); + accountContact.addAll(result2); } catch (_) { } @@ -377,7 +371,7 @@ class _ProfilePanelState extends OptimizedState with WidgetsBindin if (value == null) return; accountInfo['active_alias'] = value; setState(() {}); - await http.setAccountAlias(value); + await backend.setAccountAlias(value); }, ), ], diff --git a/lib/app/layouts/settings/pages/server/backup_restore_panel.dart b/lib/app/layouts/settings/pages/server/backup_restore_panel.dart index 6ea7ecd16..9cb5c6fe5 100644 --- a/lib/app/layouts/settings/pages/server/backup_restore_panel.dart +++ b/lib/app/layouts/settings/pages/server/backup_restore_panel.dart @@ -43,6 +43,12 @@ class _BackupRestorePanelState extends OptimizedState { } void getBackups() async { + if (backend.remoteService == null) { + setState(() { + fetching = false; + }); + return; + } final response1 = await http.getSettings().catchError((_) { setState(() { fetching = null; @@ -102,6 +108,9 @@ class _BackupRestorePanelState extends OptimizedState { } Future showMethodDialog() async { + if (backend.remoteService == null) { + return false; + } return await showDialog( context: context, builder: (BuildContext context) { diff --git a/lib/app/layouts/settings/pages/theming/avatar/avatar_crop.dart b/lib/app/layouts/settings/pages/theming/avatar/avatar_crop.dart index 262968a1e..cc41bdfea 100644 --- a/lib/app/layouts/settings/pages/theming/avatar/avatar_crop.dart +++ b/lib/app/layouts/settings/pages/theming/avatar/avatar_crop.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:universal_io/io.dart'; +import 'package:image/image.dart' as img; class AvatarCrop extends StatefulWidget { final int? index; @@ -23,7 +24,9 @@ class _AvatarCropState extends OptimizedState { Uint8List? _imageData; bool _isLoading = true; - void onCropped(Uint8List croppedData) async { + void onCropped(Uint8List sizedData) async { + var image = img.copyResize(img.decodeImage(sizedData)!, width: 570, height: 570); + var croppedData = img.encodePng(image); String appDocPath = fs.appDocDir.path; if (widget.index == null && widget.chat == null) { File file = File("$appDocPath/avatars/you/avatar-${croppedData.length}.jpg"); diff --git a/lib/app/layouts/settings/settings_page.dart b/lib/app/layouts/settings/settings_page.dart index 18051ef14..23418b6b2 100644 --- a/lib/app/layouts/settings/settings_page.dart +++ b/lib/app/layouts/settings/settings_page.dart @@ -56,7 +56,7 @@ class _SettingsPageState extends OptimizedState { void initState() { super.initState(); - if (showAltLayoutContextless) { + if (showAltLayoutContextless && backend.remoteService != null) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { ns.pushAndRemoveSettingsUntil( context, @@ -138,150 +138,151 @@ class _SettingsPageState extends OptimizedState { ), ], ), - if (!kIsWeb) + if (!kIsWeb && backend.remoteService != null) SettingsHeader( iosSubtitle: iosSubtitle, materialSubtitle: materialSubtitle, text: "Server & Message Management"), - SettingsSection( - backgroundColor: tileColor, - children: [ - Obx(() { - String? subtitle; - switch (socket.state.value) { - case SocketState.connected: - subtitle = "Connected"; - break; - case SocketState.disconnected: - subtitle = "Disconnected"; - break; - case SocketState.error: - subtitle = "Error"; - break; - case SocketState.connecting: - subtitle = "Connecting..."; - break; - default: - subtitle = "Error"; - break; - } + if (backend.remoteService != null) + SettingsSection( + backgroundColor: tileColor, + children: [ + Obx(() { + String? subtitle; + switch (socket.state.value) { + case SocketState.connected: + subtitle = "Connected"; + break; + case SocketState.disconnected: + subtitle = "Disconnected"; + break; + case SocketState.error: + subtitle = "Error"; + break; + case SocketState.connecting: + subtitle = "Connecting..."; + break; + default: + subtitle = "Error"; + break; + } - return SettingsTile( - backgroundColor: tileColor, - title: "Connection & Server", - subtitle: subtitle, - onTap: () async { - ns.pushAndRemoveSettingsUntil( - context, - ServerManagementPanel(), - (route) => route.isFirst, - ); - }, - onLongPress: () { - Clipboard.setData(ClipboardData(text: http.origin)); - if (!Platform.isAndroid || (fs.androidInfo?.version.sdkInt ?? 0) < 33) { - showSnackbar("Copied", "Server address copied to clipboard!"); - } - }, - leading: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Material( - shape: samsung ? SquircleBorder( - side: BorderSide( - color: getIndicatorColor(socket.state.value), - width: 3.0 - ), - ) : null, - color: ss.settings.skin.value != Skins.Material - ? getIndicatorColor(socket.state.value) - : Colors.transparent, - borderRadius: iOS - ? BorderRadius.circular(5) : null, - child: SizedBox( - width: 32, - height: 32, - child: Stack( - alignment: Alignment.center, - children: [ - Icon( - iOS - ? CupertinoIcons.antenna_radiowaves_left_right - : Icons.router, - color: - ss.settings.skin.value != Skins.Material ? Colors.white : Colors.grey, - size: ss.settings.skin.value != Skins.Material ? 23 : 30, + return SettingsTile( + backgroundColor: tileColor, + title: "Connection & Server", + subtitle: subtitle, + onTap: () async { + ns.pushAndRemoveSettingsUntil( + context, + ServerManagementPanel(), + (route) => route.isFirst, + ); + }, + onLongPress: () { + Clipboard.setData(ClipboardData(text: http.origin)); + if (!Platform.isAndroid || (fs.androidInfo?.version.sdkInt ?? 0) < 33) { + showSnackbar("Copied", "Server address copied to clipboard!"); + } + }, + leading: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Material( + shape: samsung ? SquircleBorder( + side: BorderSide( + color: getIndicatorColor(socket.state.value), + width: 3.0 ), - if (material) - Positioned.fill( - child: Align( - alignment: Alignment.bottomRight, - child: - getIndicatorIcon(socket.state.value, size: 15, showAlpha: false)), + ) : null, + color: ss.settings.skin.value != Skins.Material + ? getIndicatorColor(socket.state.value) + : Colors.transparent, + borderRadius: iOS + ? BorderRadius.circular(5) : null, + child: SizedBox( + width: 32, + height: 32, + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + iOS + ? CupertinoIcons.antenna_radiowaves_left_right + : Icons.router, + color: + ss.settings.skin.value != Skins.Material ? Colors.white : Colors.grey, + size: ss.settings.skin.value != Skins.Material ? 23 : 30, ), - ]), + if (material) + Positioned.fill( + child: Align( + alignment: Alignment.bottomRight, + child: + getIndicatorIcon(socket.state.value, size: 15, showAlpha: false)), + ), + ]), + ), ), - ), - ], - ), - trailing: nextIcon, - ); - }), - if (ss.serverDetailsSync().item4 >= 205) - Container( - color: tileColor, - child: Padding( - padding: const EdgeInsets.only(left: 65.0), - child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + ], + ), + trailing: nextIcon, + ); + }), + if (backend.canSchedule) + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + ), ), - ), - if (ss.serverDetailsSync().item4 >= 205) - SettingsTile( - backgroundColor: tileColor, - title: "Scheduled Messages", - subtitle: "Schedule your server to send a message in the future or at set intervals", - isThreeLine: true, - onTap: () { - ns.pushAndRemoveSettingsUntil( - context, - ScheduledMessagesPanel(), - (route) => route.isFirst, - ); - }, - trailing: nextIcon, - leading: const SettingsLeadingIcon( - iosIcon: CupertinoIcons.calendar_today, - materialIcon: Icons.schedule_send_outlined, + if (backend.canSchedule) + SettingsTile( + backgroundColor: tileColor, + title: "Scheduled Messages", + subtitle: "Schedule your server to send a message in the future or at set intervals", + isThreeLine: true, + onTap: () { + ns.pushAndRemoveSettingsUntil( + context, + ScheduledMessagesPanel(), + (route) => route.isFirst, + ); + }, + trailing: nextIcon, + leading: const SettingsLeadingIcon( + iosIcon: CupertinoIcons.calendar_today, + materialIcon: Icons.schedule_send_outlined, + ), ), - ), - if (Platform.isAndroid) - Container( - color: tileColor, - child: Padding( - padding: const EdgeInsets.only(left: 65.0), - child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + if (Platform.isAndroid) + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + ), ), - ), - if (Platform.isAndroid) - SettingsTile( - backgroundColor: tileColor, - title: "Message Reminders", - subtitle: "View and manage your upcoming message reminders", - onTap: () { - ns.pushAndRemoveSettingsUntil( - context, - MessageRemindersPanel(), - (route) => route.isFirst, - ); - }, - trailing: nextIcon, - leading: const SettingsLeadingIcon( - iosIcon: CupertinoIcons.alarm, - materialIcon: Icons.alarm, + if (Platform.isAndroid) + SettingsTile( + backgroundColor: tileColor, + title: "Message Reminders", + subtitle: "View and manage your upcoming message reminders", + onTap: () { + ns.pushAndRemoveSettingsUntil( + context, + MessageRemindersPanel(), + (route) => route.isFirst, + ); + }, + trailing: nextIcon, + leading: const SettingsLeadingIcon( + iosIcon: CupertinoIcons.alarm, + materialIcon: Icons.alarm, + ), ), - ), - ], - ), + ], + ), SettingsHeader( iosSubtitle: iosSubtitle, materialSubtitle: materialSubtitle, @@ -478,13 +479,13 @@ class _SettingsPageState extends OptimizedState { : SocketState.connecting), ), )), - Container( - color: tileColor, - child: Padding( - padding: const EdgeInsets.only(left: 65.0), - child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), + ), ), - ), Obx(() => SettingsTile( backgroundColor: tileColor, title: "Redacted Mode", @@ -591,7 +592,7 @@ class _SettingsPageState extends OptimizedState { child: SettingsDivider(color: context.theme.colorScheme.surfaceVariant), ), ), - if (!kIsWeb && !kIsDesktop) + if (!kIsWeb && !kIsDesktop && backend.remoteService != null) SettingsTile( backgroundColor: tileColor, onTap: () async { @@ -657,7 +658,7 @@ class _SettingsPageState extends OptimizedState { var map = c.toMap(); contacts.add(map); } - http.createContact(contacts, onSendProgress: (count, total) { + backend.remoteService!.createContact(contacts, onSendProgress: (count, total) { uploadingContacts.value = true; progress.value = count / total; totalSize.value = total; diff --git a/lib/app/layouts/settings/widgets/content/settings_switch.dart b/lib/app/layouts/settings/widgets/content/settings_switch.dart index f2dd18f7c..2ed80a193 100644 --- a/lib/app/layouts/settings/widgets/content/settings_switch.dart +++ b/lib/app/layouts/settings/widgets/content/settings_switch.dart @@ -11,6 +11,7 @@ class SettingsSwitch extends StatelessWidget { this.backgroundColor, this.subtitle, this.isThreeLine = false, + this.padding = true, }); final bool initialVal; final Function(bool) onChanged; @@ -18,6 +19,7 @@ class SettingsSwitch extends StatelessWidget { final Color? backgroundColor; final String? subtitle; final bool isThreeLine; + final bool padding; @override Widget build(BuildContext context) { @@ -45,7 +47,7 @@ class SettingsSwitch extends StatelessWidget { subtitle!, style: context.theme.textTheme.bodySmall!.copyWith(color: context.theme.colorScheme.properOnSurface, height: 1.5), ) : null, - contentPadding: const EdgeInsets.symmetric(horizontal: 16.0), + contentPadding: padding ? const EdgeInsets.symmetric(horizontal: 16.0) : EdgeInsets.zero, ), ), ); diff --git a/lib/helpers/network/network_error_handler.dart b/lib/helpers/network/network_error_handler.dart index 66176a2bc..35fc7ade2 100644 --- a/lib/helpers/network/network_error_handler.dart +++ b/lib/helpers/network/network_error_handler.dart @@ -20,7 +20,7 @@ Message handleSendError(dynamic error, Message m) { m.guid = m.guid!.replaceAll("temp", "error-$_error"); m.error = error.response?.statusCode ?? MessageError.BAD_REQUEST.code; } else { - m.guid = m.guid!.replaceAll("temp", "error-Connection timeout, please check your internet connection and try again"); + m.guid = m.guid!.replaceAll("temp", "error-$error"); m.error = MessageError.BAD_REQUEST.code; } diff --git a/lib/helpers/network/network_tasks.dart b/lib/helpers/network/network_tasks.dart index d2cbc4bfa..c7253963d 100644 --- a/lib/helpers/network/network_tasks.dart +++ b/lib/helpers/network/network_tasks.dart @@ -15,7 +15,7 @@ class NetworkTasks { static Future onConnect() async { if (ss.settings.finishedSetup.value) { if (cm.activeChat != null) { - socket.sendMessage("update-typing-status", {"chatGuid": cm.activeChat!.chat.guid}); + backend.updateTypingStatus(cm.activeChat!.chat); } await fcm.registerDevice(); await ss.getServerDetails(refresh: true); diff --git a/lib/helpers/types/constants.dart b/lib/helpers/types/constants.dart index 25eb71904..eda45891b 100644 --- a/lib/helpers/types/constants.dart +++ b/lib/helpers/types/constants.dart @@ -2,6 +2,8 @@ import 'package:bluebubbles/services/backend/settings/settings_service.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +enum ParticipantOp { Add, Remove } + const effectMap = { "slam": "com.apple.MobileSMS.expressivesend.impact", "loud": "com.apple.MobileSMS.expressivesend.loud", diff --git a/lib/helpers/ui/ui_helpers.dart b/lib/helpers/ui/ui_helpers.dart index 1c908126e..2f45f7a89 100644 --- a/lib/helpers/ui/ui_helpers.dart +++ b/lib/helpers/ui/ui_helpers.dart @@ -93,12 +93,13 @@ Widget buildBackButton(BuildContext context, {EdgeInsets padding = EdgeInsets.ze )); } -Widget buildProgressIndicator(BuildContext context, {double size = 20, double strokeWidth = 2}) { +Widget buildProgressIndicator(BuildContext context, {double size = 20, double strokeWidth = 2, ui.Brightness? brightness}) { + brightness ??= ThemeData.estimateBrightnessForColor(context.theme.colorScheme.background); return ss.settings.skin.value == Skins.iOS ? Theme( data: ThemeData( cupertinoOverrideTheme: - CupertinoThemeData(brightness: ThemeData.estimateBrightnessForColor(context.theme.colorScheme.background)), + CupertinoThemeData(brightness: brightness), ), child: CupertinoActivityIndicator( radius: size / 2, @@ -112,7 +113,7 @@ Widget buildProgressIndicator(BuildContext context, {double size = 20, double st height: size, child: CircularProgressIndicator( strokeWidth: strokeWidth, - valueColor: AlwaysStoppedAnimation(context.theme.colorScheme.primary), + valueColor: AlwaysStoppedAnimation(brightness == ui.Brightness.dark ? context.theme.colorScheme.onPrimary : context.theme.colorScheme.primary), ), ), ); diff --git a/lib/models/global/message_summary_info.dart b/lib/models/global/message_summary_info.dart index 414add25e..3e7177d68 100644 --- a/lib/models/global/message_summary_info.dart +++ b/lib/models/global/message_summary_info.dart @@ -8,6 +8,13 @@ class MessageSummaryInfo { required this.editedParts, }); + factory MessageSummaryInfo.empty() => MessageSummaryInfo( + retractedParts: [], + editedContent: {}, + originalTextRange: {}, + editedParts: [] + ); + List retractedParts; Map> editedContent; Map> originalTextRange; diff --git a/lib/models/global/platform_file.dart b/lib/models/global/platform_file.dart index 365aaa871..17d602f93 100644 --- a/lib/models/global/platform_file.dart +++ b/lib/models/global/platform_file.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:typed_data'; class PlatformFile { @@ -40,6 +41,17 @@ class PlatformFile { /// File extension for this file. String? get extension => name.split('.').last; + Future getBytes() async { + if (bytes != null) return bytes!; + final File myFile = File(path!); + return await myFile.readAsBytes(); + } + + bool exists() { + if (bytes != null) return true; + return File(path!).existsSync(); + } + @override bool operator ==(Object other) { if (identical(this, other)) { diff --git a/lib/models/global/queue_items.dart b/lib/models/global/queue_items.dart index c507156e0..2c222369b 100644 --- a/lib/models/global/queue_items.dart +++ b/lib/models/global/queue_items.dart @@ -16,7 +16,7 @@ class IncomingItem extends QueueItem { Message message; String? tempGuid; - IncomingItem._({ + IncomingItem({ required super.type, super.completer, required this.chat, @@ -25,7 +25,7 @@ class IncomingItem extends QueueItem { }); factory IncomingItem.fromMap(QueueType t, Map m, [Completer? c]) { - return IncomingItem._( + return IncomingItem( type: t, completer: c, chat: Chat.fromMap(m['chats'].first.cast()), diff --git a/lib/models/html/chat.dart b/lib/models/html/chat.dart index 873c6a2b5..dbb9b4406 100644 --- a/lib/models/html/chat.dart +++ b/lib/models/html/chat.dart @@ -285,12 +285,10 @@ class Chat { } try { - if (privateMark && ss.settings.enablePrivateAPI.value && ss.settings.privateMarkChatAsRead.value) { - if (!hasUnread && autoSendReadReceipts!) { - http.markChatRead(guid); - } else if (hasUnread) { - http.markChatUnread(guid); - } + if (!hasUnread && autoSendReadReceipts!) { + backend.markRead(this, privateMark && ss.settings.enablePrivateAPI.value && ss.settings.privateMarkChatAsRead.value); + } else if (hasUnread) { + backend.markUnread(this); } } catch (_) {} @@ -452,7 +450,7 @@ class Chat { this.autoSendReadReceipts = autoSendReadReceipts; save(updateAutoSendReadReceipts: true); if (autoSendReadReceipts ?? ss.settings.privateMarkChatAsRead.value) { - http.markChatRead(guid); + backend.markRead(this, ss.settings.privateMarkChatAsRead.value); } return this; } @@ -462,7 +460,7 @@ class Chat { this.autoSendTypingIndicators = autoSendTypingIndicators; save(updateAutoSendTypingIndicators: true); if (!(autoSendTypingIndicators ?? ss.settings.privateSendTypingIndicators.value)) { - socket.sendMessage("stopped-typing", {"chatGuid": guid}); + backend.stoppedTyping(this); } return this; } diff --git a/lib/models/io/attachment.dart b/lib/models/io/attachment.dart index ddcd32975..4df69d2b0 100644 --- a/lib/models/io/attachment.dart +++ b/lib/models/io/attachment.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:bluebubbles/helpers/helpers.dart'; import 'package:bluebubbles/main.dart'; +import 'package:bluebubbles/models/global/platform_file.dart'; import 'package:bluebubbles/objectbox.g.dart'; import 'package:bluebubbles/models/io/message.dart'; import 'package:bluebubbles/services/services.dart'; @@ -88,6 +89,16 @@ class Attachment { ); } + PlatformFile getFile() { + return PlatformFile(name: transferName!, bytes: bytes, path: kIsWeb ? null : path, size: totalBytes ?? 0); + } + + Future writeToDisk() async { + var file = File(path); + await file.create(recursive: true); + await file.writeAsBytes(bytes!); + } + /// Save a new attachment or update an existing attachment on disk /// [message] is used to create a link between the attachment and message, /// when provided diff --git a/lib/models/io/chat.dart b/lib/models/io/chat.dart index 1c07ef980..38ba640f9 100644 --- a/lib/models/io/chat.dart +++ b/lib/models/io/chat.dart @@ -400,6 +400,14 @@ class Chat { ); } + void removeProfilePhoto() { + try { + File file = File(customAvatarPath!); + file.delete(); + } catch (_) {} + customAvatarPath = null; + } + /// Save a chat to the DB Chat save({ bool updateMuteType = false, @@ -521,10 +529,14 @@ class Chat { // generate names for group chats or DMs List titles = participants.map((e) => e.displayName.trim().split(isGroup && e.contact != null ? " " : String.fromCharCode(65532)).first).toList(); if (titles.isEmpty) { - if (chatIdentifier!.startsWith("urn:biz")) { - return "Business Chat"; + if(chatIdentifier != null) { + if (chatIdentifier!.startsWith("urn:biz")) { + return "Business Chat"; + } + return chatIdentifier!; + } else { + return "Unnamed chat"; } - return chatIdentifier!; } else if (titles.length == 1) { return titles[0]; } else if (titles.length <= 4) { @@ -653,13 +665,13 @@ class Chat { if (clearLocalNotifications && !hasUnread && !ls.isBubble) { mcs.invokeMethod("delete-notification", {"notification_id": id}); } - if (privateMark && ss.settings.enablePrivateAPI.value && (autoSendReadReceipts ?? ss.settings.privateMarkChatAsRead.value)) { - if (!hasUnread) { - http.markChatRead(guid); - } else if (hasUnread) { - http.markChatUnread(guid); - } - } + if (privateMark) { + if (!hasUnread) { + backend.markRead(this, ss.settings.enablePrivateAPI.value && (autoSendReadReceipts ?? ss.settings.privateMarkChatAsRead.value)); + } else if (hasUnread) { + backend.markUnread(this); + } + } } catch (_) {} return this; @@ -872,9 +884,7 @@ class Chat { if (id == null) return this; this.autoSendReadReceipts = autoSendReadReceipts; save(updateAutoSendReadReceipts: true); - if (autoSendReadReceipts ?? ss.settings.privateMarkChatAsRead.value) { - http.markChatRead(guid); - } + backend.markRead(this, autoSendReadReceipts ?? ss.settings.privateMarkChatAsRead.value); return this; } @@ -883,7 +893,7 @@ class Chat { this.autoSendTypingIndicators = autoSendTypingIndicators; save(updateAutoSendTypingIndicators: true); if (!(autoSendTypingIndicators ?? ss.settings.privateSendTypingIndicators.value)) { - socket.sendMessage("stopped-typing", {"chatGuid": guid}); + backend.stoppedTyping(this); } return this; } @@ -1004,9 +1014,13 @@ class Chat { return -(a.latestMessage.dateCreated)!.compareTo(b.latestMessage.dateCreated!); } + String _getIconPath(int responseLength) { + return "${fs.appDocDir.path}/avatars/${guid.characters.where((char) => char.isAlphabetOnly || char.isNumericOnly).join()}/avatar-$responseLength.jpg"; + } + static Future getIcon(Chat c, {bool force = false}) async { - if (!force && c.lockChatIcon) return; - final response = await http.getChatIcon(c.guid).catchError((err) async { + if ((!force && c.lockChatIcon) || backend.remoteService == null) return; + final response = await backend.remoteService!.getChatIcon(c.guid).catchError((err) async { Logger.error("Failed to get chat icon for chat ${c.getTitle()}"); return Response(statusCode: 500, requestOptions: RequestOptions(path: "")); }); @@ -1018,7 +1032,7 @@ class Chat { } } else { Logger.debug("Got chat icon for chat ${c.getTitle()}"); - File file = File("${fs.appDocDir.path}/avatars/${c.guid.characters.where((char) => char.isAlphabetOnly || char.isNumericOnly).join()}/avatar-${response.data.length}.jpg"); + File file = File(c._getIconPath(response.data.length)); if (!(await file.exists())) { await file.create(recursive: true); } diff --git a/lib/models/io/handle.dart b/lib/models/io/handle.dart index f2ef06d25..c6d3aa84a 100644 --- a/lib/models/io/handle.dart +++ b/lib/models/io/handle.dart @@ -100,6 +100,13 @@ class Handle { defaultEmail: json["defaultEmail"], ); + @override + int get hashCode => Object.hash(address, service); + + @override + bool operator== (Object other) => + other is Handle && other.address == address && other.service == service; + /// Save a single handle - prefer [bulkSave] for multiple handles rather /// than iterating through them Handle save({bool updateColor = false, matchOnOriginalROWID = false}) { diff --git a/lib/models/io/message.dart b/lib/models/io/message.dart index a5b961f58..14278980f 100644 --- a/lib/models/io/message.dart +++ b/lib/models/io/message.dart @@ -452,6 +452,13 @@ class Message { if (existing != null) { id = existing.id; text ??= existing.text; + // delivered can sometimes come before sending, don't let it get overwritten + if (existing.dateDelivered != null && dateDelivered == null) { + dateDelivered = existing.dateDelivered; + } + if (existing.dateRead != null && dateRead == null) { + dateRead = existing.dateRead; + } } // Save the participant & set the handle ID to the new participant diff --git a/lib/services/backend/action_handler.dart b/lib/services/backend/action_handler.dart index c1273beec..c29527d0b 100644 --- a/lib/services/backend/action_handler.dart +++ b/lib/services/backend/action_handler.dart @@ -69,23 +69,7 @@ class ActionHandler extends GetxService { Future sendMessage(Chat c, Message m, Message? selected, String? r) async { final completer = Completer(); if (r == null) { - http.sendMessage( - c.guid, - m.guid!, - m.text!, - subject: m.subject, - method: (ss.settings.enablePrivateAPI.value - && ss.settings.privateAPISend.value) - || (m.subject?.isNotEmpty ?? false) - || m.threadOriginatorGuid != null - || m.expressiveSendStyleId != null - ? "private-api" : "apple-script", - selectedMessageGuid: m.threadOriginatorGuid, - effectId: m.expressiveSendStyleId, - partIndex: int.tryParse(m.threadOriginatorPart?.split(":").firstOrNull ?? ""), - ddScan: m.text!.isURL, - ).then((response) async { - final newMessage = Message.fromMap(response.data['data']); + backend.sendMessage(c, m).then((newMessage) async { try { await Message.replaceMessage(m.guid, newMessage); Logger.info("Message match: [${newMessage.text}] - ${newMessage.guid} - ${m.guid}", tag: "MessageStatus"); @@ -106,8 +90,7 @@ class ActionHandler extends GetxService { completer.completeError(error); }); } else { - http.sendTapback(c.guid, selected!.text ?? "", selected.guid!, r, partIndex: m.associatedMessagePart).then((response) async { - final newMessage = Message.fromMap(response.data['data']); + backend.sendTapback(c, selected!, r, m.associatedMessagePart).then((newMessage) async { try { await Message.replaceMessage(m.guid, newMessage); Logger.info("Reaction match: [${newMessage.text}] - ${newMessage.guid} - ${m.guid}", tag: "MessageStatus"); @@ -131,45 +114,6 @@ class ActionHandler extends GetxService { return completer.future; } - - Future sendMultipart(Chat c, Message m, Message? selected, String? r) async { - final completer = Completer(); - http.sendMultipart( - c.guid, - m.guid!, - m.attributedBody.first.runs.map((e) => { - "text": m.attributedBody.first.string.substring(e.range.first, e.range.first + e.range.last), - "mention": e.attributes!.mention, - "partIndex": e.attributes!.messagePart, - }).toList(), - subject: m.subject, - selectedMessageGuid: m.threadOriginatorGuid, - effectId: m.expressiveSendStyleId, - partIndex: int.tryParse(m.threadOriginatorPart?.split(":").firstOrNull ?? ""), - ).then((response) async { - final newMessage = Message.fromMap(response.data['data']); - try { - await Message.replaceMessage(m.guid, newMessage); - Logger.info("Message match: [${newMessage.text}] - ${newMessage.guid} - ${m.guid}", tag: "MessageStatus"); - } catch (_) { - Logger.info("Message match failed for ${newMessage.guid} - already handled?", tag: "MessageStatus"); - } - completer.complete(); - }).catchError((error) async { - Logger.error('Failed to send message! Error: ${error.toString()}'); - - final tempGuid = m.guid; - m = handleSendError(error, m); - - if (!ls.isAlive || !(cm.getChatController(c.guid)?.isAlive ?? false)) { - await notif.createFailedToSend(c); - } - await Message.replaceMessage(tempGuid, m); - completer.completeError(error); - }); - - return completer.future; - } Future prepAttachment(Chat c, Message m) async { final attachment = m.attachments.first!; @@ -193,25 +137,11 @@ class ActionHandler extends GetxService { final progress = attachmentProgress.firstWhere((e) => e.item1 == attachment.guid); final completer = Completer(); latestCancelToken = CancelToken(); - http.sendAttachment( - c.guid, - attachment.guid!, - PlatformFile(name: attachment.transferName!, bytes: attachment.bytes, path: kIsWeb ? null : attachment.path, size: attachment.totalBytes ?? 0), - onSendProgress: (count, total) => progress.item2.value = count / attachment.bytes!.length, - method: (ss.settings.enablePrivateAPI.value - && ss.settings.privateAPIAttachmentSend.value) - || (m.subject?.isNotEmpty ?? false) - || m.threadOriginatorGuid != null - || m.expressiveSendStyleId != null - ? "private-api" : "apple-script", - selectedMessageGuid: m.threadOriginatorGuid, - effectId: m.expressiveSendStyleId, - partIndex: int.tryParse(m.threadOriginatorPart?.split(":").firstOrNull ?? ""), - isAudioMessage: isAudioMessage, + backend.sendAttachment( + c, m, isAudioMessage, attachment, onSendProgress: (count, total) => progress.item2.value = count / attachment.bytes!.length, cancelToken: latestCancelToken, - ).then((response) async { + ).then((newMessage) async { latestCancelToken = null; - final newMessage = Message.fromMap(response.data['data']); for (Attachment? a in newMessage.attachments) { if (a == null) continue; diff --git a/lib/services/backend/queue/outgoing_queue.dart b/lib/services/backend/queue/outgoing_queue.dart index 1d558d54d..b026c8740 100644 --- a/lib/services/backend/queue/outgoing_queue.dart +++ b/lib/services/backend/queue/outgoing_queue.dart @@ -31,11 +31,7 @@ class OutgoingQueue extends Queue { switch (item.type) { case QueueType.sendMessage: - if (item.message.attributedBody.isNotEmpty) { - await ah.sendMultipart(item.chat, item.message, item.selected, null); - } else { - await ah.sendMessage(item.chat, item.message, item.selected, item.reaction); - } + await ah.sendMessage(item.chat, item.message, item.selected, item.reaction); break; case QueueType.sendAttachment: await ah.sendAttachment(item.chat, item.message, item.customArgs?['audio'] ?? false); diff --git a/lib/services/backend/settings/settings_service.dart b/lib/services/backend/settings/settings_service.dart index e09c16293..e34caa6fe 100644 --- a/lib/services/backend/settings/settings_service.dart +++ b/lib/services/backend/settings/settings_service.dart @@ -290,9 +290,9 @@ class SettingsService extends GetxService { } Future checkServerUpdate() async { - final response = await http.checkUpdate(); - if (response.statusCode == 200) { - bool available = response.data['data']['available'] ?? false; + final response = await backend.remoteService?.checkUpdate(); + if (response?.statusCode == 200) { + bool available = response!.data['data']['available'] ?? false; Map metadata = response.data['data']['metadata'] ?? {}; if (!available || prefs.getString("server-update-check") == metadata['version']) return; showDialog( diff --git a/lib/services/backend/setup/setup_service.dart b/lib/services/backend/setup/setup_service.dart index 3bab6dd1d..0235e3b0c 100644 --- a/lib/services/backend/setup/setup_service.dart +++ b/lib/services/backend/setup/setup_service.dart @@ -10,10 +10,10 @@ class SetupService extends GetxService { sync.skipEmptyChats = skipEmptyChats; sync.saveToDownloads = saveToDownloads; await sync.startFullSync(); - await _finishSetup(); + await finishSetup(); } - Future _finishSetup() async { + Future finishSetup() async { ss.settings.finishedSetup.value = true; await ss.saveSettings(); await NetworkTasks.onConnect(); diff --git a/lib/services/backend/sync/sync_service.dart b/lib/services/backend/sync/sync_service.dart index d4bd49219..45be4c9f6 100644 --- a/lib/services/backend/sync/sync_service.dart +++ b/lib/services/backend/sync/sync_service.dart @@ -29,6 +29,10 @@ class SyncService extends GetxService { Future startFullSync() async { // Set the last sync date (for incremental, even though this isn't incremental) // We won't try an incremental sync until the last (full) sync date is set + if (backend.remoteService == null) { + await cs.refreshContacts(); + return; // no syncing if no remote + } ss.settings.lastIncrementalSync.value = DateTime.now().millisecondsSinceEpoch; await ss.saveSettings(); @@ -41,6 +45,9 @@ class SyncService extends GetxService { } Future startIncrementalSync() async { + if (backend.remoteService == null) { + await chats.init(); + } isIncrementalSyncing.value = true; final contacts = []; @@ -69,7 +76,7 @@ class SyncService extends GetxService { if (result.isNotEmpty && (result.first.isNotEmpty || result.last.isNotEmpty)) { contacts.addAll(Contact.getContacts()); // auto upload contacts if requested - if (ss.settings.syncContactsAutomatically.value) { + if (ss.settings.syncContactsAutomatically.value && backend.remoteService != null) { Logger.debug("Contact changes detected, uploading to server..."); final _contacts = >[]; for (Contact c in contacts) { @@ -116,6 +123,14 @@ Future>> incrementalSyncIsolate(List? items) async { http.originOverride = address; } + if (backend.remoteService == null) { + // just do contacts + final refreshedItems = await cs.refreshContacts(); + Logger.info('Finished contact refresh, shouldRefresh $refreshedItems'); + port?.send(refreshedItems); + return refreshedItems; + } + int syncStart = ss.settings.lastIncrementalSync.value; int startRowId = ss.settings.lastIncrementalSyncRowId.value; final incrementalSyncManager = IncrementalSyncManager( diff --git a/lib/services/network/backend_service.dart b/lib/services/network/backend_service.dart new file mode 100644 index 000000000..f632e579c --- /dev/null +++ b/lib/services/network/backend_service.dart @@ -0,0 +1,49 @@ +import 'package:bluebubbles/models/models.dart'; +import 'package:bluebubbles/services/services.dart'; +import 'package:dio/dio.dart'; +import 'package:get/get.dart'; + +BackendService backend = Get.isRegistered() ? Get.find() : Get.put(HttpBackend()); + +abstract class BackendService extends GetxService { + Future createChat(List addresses, String? message, String service, + {CancelToken? cancelToken}); + Future sendMessage(Chat c, Message m, {CancelToken? cancelToken}); + Future renameChat(Chat chat, String newName); + Future chatParticipant(ParticipantOp method, Chat chat, String newName); + Future leaveChat(Chat chat); + Future sendTapback( + Chat chat, Message selected, String reaction, int? repPart); + Future markRead(Chat chat, bool notifyOthers); + Future markUnread(Chat chat); + HttpService? get remoteService; + bool get canLeaveChat; + bool get canEditUnsend; + Future unsend(Message msg, MessagePart part); + Future edit(Message msgGuid, String text, int part); + Future downloadAttachment(Attachment attachment, + {void Function(int, int)? onReceiveProgress, bool original = false, CancelToken? cancelToken}); + // returns the new message that was sent + Future sendAttachment(Chat c, Message m, bool isAudioMessage, Attachment attachment, + {void Function(int, int)? onSendProgress, CancelToken? cancelToken}); + bool canCancelUploads(); + Future canUploadGroupPhotos(); + Future setChatIcon(Chat chat, + {void Function(int, int)? onSendProgress, CancelToken? cancelToken}); + Future deleteChatIcon(Chat chat, {CancelToken? cancelToken}); + bool supportsFocusStates(); + Future downloadLivePhoto(Attachment att, String target, + {void Function(int, int)? onReceiveProgress, CancelToken? cancelToken}); + bool get canSchedule; + bool get supportsFindMy; + bool get canCreateGroupChats; + bool get supportsSmsForwarding; + void startedTyping(Chat c); + void stoppedTyping(Chat c); + void updateTypingStatus(Chat c); + Future handleiMessageState(String address); + Future> getAccountInfo(); + Future setDefaultHandle(String handle); + Future> getAccountContact(); + void init(); +} diff --git a/lib/services/network/downloads_service.dart b/lib/services/network/downloads_service.dart index 5f87bcc20..8b6fd61c7 100644 --- a/lib/services/network/downloads_service.dart +++ b/lib/services/network/downloads_service.dart @@ -96,8 +96,11 @@ class AttachmentDownloadController extends GetxController { if (attachment.guid == null || attachment.guid!.contains("temp")) return; isFetching = true; stopwatch.start(); - var response = await http.downloadAttachment(attachment.guid!, - onReceiveProgress: (count, total) => setProgress(kIsWeb ? (count / total) : (count / attachment.totalBytes!))).catchError((err) async { + PlatformFile response; + try { + response = await backend.downloadAttachment(attachment, + onReceiveProgress: (count, total) => setProgress(kIsWeb ? (count / total) : (count / attachment.totalBytes!))); + } catch (e) { if (!kIsWeb) { File file = File(attachment.path); if (await file.exists()) { @@ -110,20 +113,13 @@ class AttachmentDownloadController extends GetxController { error.value = true; attachmentDownloader._removeFromQueue(this); - return Response(requestOptions: RequestOptions(path: '')); - }); - if (response.statusCode != 200) return; - Uint8List bytes; - if (attachment.mimeType == "image/gif") { - bytes = await fixSpeedyGifs(response.data); - } else { - bytes = response.data; + return; } - if (!kIsWeb && !kIsDesktop) { + if (!kIsWeb && !kIsDesktop && response.path == null) { File _file = await File(attachment.path).create(recursive: true); - await _file.writeAsBytes(bytes); + await _file.writeAsBytes(response.bytes!); + response.path = attachment.path; } - attachment.webUrl = response.requestOptions.path; Logger.info("Finished fetching attachment"); stopwatch.stop(); Logger.info("Attachment downloaded in ${stopwatch.elapsedMilliseconds} ms"); @@ -140,15 +136,9 @@ class AttachmentDownloadController extends GetxController { // Finish the downloader attachmentDownloader._removeFromQueue(this); - attachment.bytes = bytes; // Add attachment to sink based on if we got data - file.value = PlatformFile( - name: attachment.transferName!, - path: kIsWeb ? null : attachment.path, - size: bytes.length, - bytes: bytes, - ); + file.value = response; for (Function f in completeFuncs) { f.call(file.value); } diff --git a/lib/services/network/http_backend.dart b/lib/services/network/http_backend.dart new file mode 100644 index 000000000..fd047c81e --- /dev/null +++ b/lib/services/network/http_backend.dart @@ -0,0 +1,259 @@ + +import 'package:bluebubbles/models/models.dart'; +import 'package:bluebubbles/services/services.dart'; + +class HttpBackend extends BackendService { + @override + Future createChat(List addresses, String? message, String service, {CancelToken? cancelToken}) async { + var response = await http.createChat(addresses, message, service, cancelToken: cancelToken); + return Chat.fromMap(response.data["data"]); + } + + @override + void init() { } + + @override + void startedTyping(Chat c) { + socket.sendMessage("started-typing", {"chatGuid": c.guid}); + } + + @override + void stoppedTyping(Chat c){ + socket.sendMessage("stopped-typing", {"chatGuid": c.guid}); + } + + @override + void updateTypingStatus(Chat c) { + socket.sendMessage("update-typing-status", {"chatGuid": c.guid}); + } + + @override + Future> getAccountInfo() async { + var result = await http.getAccountInfo(); + if (!isNullOrEmpty(result.data.isNotEmpty)!) { + return result.data['data']; + } + return {}; + } + + @override + Future setDefaultHandle(String defaultHandle) async { + await http.setAccountAlias(defaultHandle); + } + + @override + Future> getAccountContact() async { + if (ss.isMinBigSurSync) { + final result2 = await http.getAccountContact(); + if (!isNullOrEmpty(result2.data.isNotEmpty)!) { + return result2.data['data']; + } + } + return {}; + } + + @override + Future sendMessage(Chat c, Message m, {CancelToken? cancelToken}) async { + if (m.attributedBody.isNotEmpty) { + var response = await http.sendMultipart( + c.guid, + m.guid!, + m.attributedBody.first.runs.map((e) => { + "text": m.attributedBody.first.string.substring(e.range.first, e.range.first + e.range.last), + "mention": e.attributes!.mention, + "partIndex": e.attributes!.messagePart, + }).toList(), + subject: m.subject, + selectedMessageGuid: m.threadOriginatorGuid, + effectId: m.expressiveSendStyleId, + partIndex: int.tryParse(m.threadOriginatorPart?.split(":").firstOrNull ?? ""), + ); + return Message.fromMap(response.data["data"]); + } else { + var response = await http.sendMessage(c.guid, + m.guid!, + m.text!, + subject: m.subject, + method: (ss.settings.enablePrivateAPI.value + && ss.settings.privateAPISend.value) + || (m.subject?.isNotEmpty ?? false) + || m.threadOriginatorGuid != null + || m.expressiveSendStyleId != null + ? "private-api" : "apple-script", + selectedMessageGuid: m.threadOriginatorGuid, + effectId: m.expressiveSendStyleId, + partIndex: int.tryParse(m.threadOriginatorPart?.split(":").firstOrNull ?? ""), + ddScan: m.text!.isURL, cancelToken: cancelToken); + return Message.fromMap(response.data["data"]); + } + } + + @override + Future renameChat(Chat chat, String newName) async { + return (await http.updateChat(chat.guid, newName)).statusCode == 200; + } + + @override + Future chatParticipant(ParticipantOp op, Chat chat, String address) async { + var method = op.name.toLowerCase(); + return (await http.chatParticipant(method, chat.guid, address)).statusCode == 200; + } + + @override + Future leaveChat(Chat chat) async { + return (await http.leaveChat(chat.guid)).statusCode == 200; + } + + @override + Future sendTapback(Chat chat, Message selected, String reaction, int? repPart) async { + return Message.fromMap((await http.sendTapback(chat.guid, selected.text ?? "", selected.guid!, reaction, partIndex: repPart)).data['data']); + } + + @override + Future markRead(Chat chat, bool notifyOthers) async { + if (!notifyOthers) return true; + return (await http.markChatRead(chat.guid)).statusCode == 200; + } + + @override + Future markUnread(Chat chat) async { + return (await http.markChatUnread(chat.guid)).statusCode == 200; + } + + @override + HttpService? remoteService { + return http; + } + + @override + bool get canLeaveChat { + return ss.serverDetailsSync().item4 >= 226; + } + + @override + bool get canEditUnsend { + return ss.isMinVenturaSync && ss.serverDetailsSync().item4 >= 148; + } + + @override + Future unsend(Message msg, MessagePart part) async { + var response = await http.unsend(msg.guid!, partIndex: part.part); + if (response.statusCode != 200) { + return null; + } + return Message.fromMap(response.data['data']); + } + + @override + Future edit(Message msg, String text, int part) async { + var response = await http.edit(msg.guid!, text, "Edited to: “$text", partIndex: part); + if (response.statusCode != 200) { + return null; + } + return Message.fromMap(response.data['data']); + } + + @override + Future downloadAttachment(Attachment att, {void Function(int p1, int p2)? onReceiveProgress, bool original = false, CancelToken? cancelToken}) async { + var response = await http.downloadAttachment(att.guid!, onReceiveProgress: onReceiveProgress, original: original, cancelToken: cancelToken); + if (response.statusCode != 200) { + throw Exception("Bad!"); + } + if (att.mimeType == "image/gif") { + att.bytes = await fixSpeedyGifs(response.data); + } else { + att.bytes = response.data; + } + att.webUrl = response.requestOptions.path; + return att.getFile(); + } + + @override + Future sendAttachment(Chat c, Message m, bool isAudioMessage, Attachment attachment, {void Function(int p1, int p2)? onSendProgress, CancelToken? cancelToken}) async { + var response = await http.sendAttachment(c.guid, + attachment.guid!, + attachment.getFile(), + onSendProgress: onSendProgress, + method: (ss.settings.enablePrivateAPI.value + && ss.settings.privateAPIAttachmentSend.value) + || (m.subject?.isNotEmpty ?? false) + || m.threadOriginatorGuid != null + || m.expressiveSendStyleId != null + ? "private-api" : "apple-script", + selectedMessageGuid: m.threadOriginatorGuid, + effectId: m.expressiveSendStyleId, + partIndex: int.tryParse(m.threadOriginatorPart?.split(":").firstOrNull ?? ""), + isAudioMessage: isAudioMessage, + cancelToken: cancelToken); + if (response.statusCode != 200) { + throw Exception("Failed to upload!"); + } + return Message.fromMap(response.data['data']); + } + + @override + bool canCancelUploads() { + return true; + } + + @override + Future canUploadGroupPhotos() async { + return (await ss.isMinBigSur) && ss.serverDetailsSync().item4 >= 226; + } + + @override + Future setChatIcon(Chat chat, {void Function(int, int)? onSendProgress, CancelToken? cancelToken}) async { + return (await http.setChatIcon(chat.guid, chat.customAvatarPath!, onSendProgress: onSendProgress, cancelToken: cancelToken)).statusCode == 200; + } + + @override + Future deleteChatIcon(Chat chat, {CancelToken? cancelToken}) async { + return (await http.deleteChatIcon(chat.guid, cancelToken: cancelToken)).statusCode == 200; + } + + @override + bool supportsFocusStates() { + return ss.isMinMontereySync; + } + + @override + Future downloadLivePhoto(Attachment att, String target, {void Function(int p1, int p2)? onReceiveProgress, CancelToken? cancelToken}) async { + var response = await http.downloadLivePhoto(att.guid!, onReceiveProgress: onReceiveProgress, cancelToken: cancelToken); + if (response.statusCode != 200) { + return false; + } + final file = PlatformFile( + name: target, + size: response.data.length, + bytes: response.data, + ); + await as.saveToDisk(file); + return true; + } + + @override + bool get canSchedule { + return ss.serverDetailsSync().item4 >= 205; + } + + @override + bool get supportsFindMy { + return ss.isMinCatalinaSync; + } + + @override + bool get canCreateGroupChats { + return ss.canCreateGroupChatSync(); + } + + @override + bool get supportsSmsForwarding { + return true; + } + + @override + Future handleiMessageState(String address) async { + final response = await http.handleiMessageState(address); + return response.data["data"]["available"]; + } +} \ No newline at end of file diff --git a/lib/services/network/http_service.dart b/lib/services/network/http_service.dart index eb8f7f432..d38c3f984 100644 --- a/lib/services/network/http_service.dart +++ b/lib/services/network/http_service.dart @@ -1,10 +1,13 @@ +import 'package:bluebubbles/helpers/helpers.dart'; import 'package:bluebubbles/models/models.dart'; +import 'package:bluebubbles/utils/file_utils.dart'; import 'package:bluebubbles/utils/logger.dart'; import 'package:bluebubbles/services/services.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart' hide Response, FormData, MultipartFile; +import 'backend_service.dart'; /// Get an instance of our [HttpService] HttpService http = Get.isRegistered() ? Get.find() : Get.put(HttpService()); @@ -33,6 +36,7 @@ class HttpService extends GetxService { /// Global try-catch function Future runApiGuarded(Future Function() func, {bool checkOrigin = true}) async { if (http.origin.isEmpty && checkOrigin) { + Logger.error("Api failed ${StackTrace.current}"); return Future.error("No server URL!"); } try { @@ -1140,15 +1144,13 @@ class HttpService extends GetxService { } Future downloadFromUrl(String url, {Function(int, int)? progress, CancelToken? cancelToken}) async { - return runApiGuarded(() async { - final response = await dio.get( - url, - options: Options(responseType: ResponseType.bytes, receiveTimeout: dio.options.receiveTimeout! * 12, headers: headers), - cancelToken: cancelToken, - onReceiveProgress: progress, - ); - return returnSuccessOrError(response); - }); + final response = await dio.get( + url, + options: Options(responseType: ResponseType.bytes, receiveTimeout: dio.options.receiveTimeout! * 12, headers: headers), + cancelToken: cancelToken, + onReceiveProgress: progress, + ); + return returnSuccessOrError(response); } // The following methods are for Firebase only diff --git a/lib/services/network/socket_service.dart b/lib/services/network/socket_service.dart index 8e588d9d1..294386c1a 100644 --- a/lib/services/network/socket_service.dart +++ b/lib/services/network/socket_service.dart @@ -128,7 +128,7 @@ class SocketService extends GetxService { socket.emitWithAck(event, message, ack: (response) { if (response['encrypted'] == true) { - response['data'] = jsonDecode(decryptAESCryptoJS(response['data'], password)); + response['data'] = jsonDecode(utf8.decode(decryptAESCryptoJS(response['data'], password))); } if (!completer.isCompleted) { diff --git a/lib/services/services.dart b/lib/services/services.dart index 61d8d7a16..bbe822bd4 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -23,6 +23,7 @@ export 'network/firebase/database_service.dart'; export 'network/downloads_service.dart'; export 'network/http_service.dart'; export 'network/socket_service.dart'; +export 'network/backend_service.dart'; export 'ui/chat/chat_lifecycle_manager.dart'; export 'ui/chat/chat_manager.dart'; export 'ui/chat/chats_service.dart'; diff --git a/lib/services/ui/chat/chat_manager.dart b/lib/services/ui/chat/chat_manager.dart index 1f139b62d..c488f908d 100644 --- a/lib/services/ui/chat/chat_manager.dart +++ b/lib/services/ui/chat/chat_manager.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:bluebubbles/main.dart'; import 'package:bluebubbles/utils/logger.dart'; import 'package:bluebubbles/models/models.dart'; import 'package:bluebubbles/services/services.dart'; @@ -95,13 +96,18 @@ class ChatManager extends GetxService { /// Fetch chat information from the server Future fetchChat(String chatGuid, {withParticipants = true, withLastMessage = false}) async { - Logger.info("Fetching full chat metadata from server.", tag: "Fetch-Chat"); + Logger.info("Fetching full chat metadata from server. ${StackTrace.current}", tag: "Fetch-Chat"); + + var remote = backend.remoteService; + if (remote == null) { + return Chat.findOne(guid: chatGuid); + } final withQuery = []; if (withParticipants) withQuery.add("participants"); if (withLastMessage) withQuery.add("lastmessage"); - final response = await http.singleChat(chatGuid, withQuery: withQuery.join(",")).catchError((err) { + final response = await remote.singleChat(chatGuid, withQuery: withQuery.join(",")).catchError((err) { if (err is! Response) { Logger.error("Failed to fetch chat metadata! ${err.toString()}", tag: "Fetch-Chat"); return err; @@ -174,7 +180,7 @@ class ChatManager extends GetxService { if (withAttachment) withQuery.add("attachment"); if (withHandle) withQuery.add("handle"); - http.chatMessages(guid, withQuery: withQuery.join(","), offset: offset, limit: limit, sort: sort, after: after, before: before).then((response) { + backend.remoteService?.chatMessages(guid, withQuery: withQuery.join(","), offset: offset, limit: limit, sort: sort, after: after, before: before).then((response) { if (!completer.isCompleted) completer.complete(response.data["data"]); }).catchError((err) { late final dynamic error; @@ -184,7 +190,7 @@ class ChatManager extends GetxService { error = err.toString(); } if (!completer.isCompleted) completer.completeError(error); - }); + }) ?? completer.complete([]); return completer.future; } diff --git a/lib/services/ui/chat/chats_service.dart b/lib/services/ui/chat/chats_service.dart index f407387c0..c2cddb357 100644 --- a/lib/services/ui/chat/chats_service.dart +++ b/lib/services/ui/chat/chats_service.dart @@ -61,10 +61,10 @@ class ChatsService extends GetxService { Future init({bool force = false}) async { if (!force && !ss.settings.finishedSetup.value) return; Logger.info("Fetching chats...", tag: "ChatBloc"); - currentCount = Chat.count() ?? (await http.chatCount().catchError((err) { + currentCount = Chat.count() ?? (await backend.remoteService?.chatCount().catchError((err) { Logger.info("Error when fetching chat count!", tag: "ChatBloc"); return Response(requestOptions: RequestOptions(path: '')); - })).data['data']['total'] ?? 0; + }))?.data['data']['total'] ?? 0; loadedAllChats = Completer(); if (currentCount != 0) { hasChats.value = true; @@ -178,9 +178,7 @@ class ChatsService extends GetxService { for (Chat c in _chats) { c.hasUnreadMessage = false; mcs.invokeMethod("delete-notification", {"notification_id": c.id}); - if (ss.settings.enablePrivateAPI.value && ss.settings.privateMarkChatAsRead.value) { - http.markChatRead(c.guid); - } + backend.markRead(c, ss.settings.enablePrivateAPI.value && ss.settings.privateMarkChatAsRead.value); } chatBox.putMany(_chats); } diff --git a/lib/utils/crypto_utils.dart b/lib/utils/crypto_utils.dart index 54c171908..9324f2428 100644 --- a/lib/utils/crypto_utils.dart +++ b/lib/utils/crypto_utils.dart @@ -6,7 +6,7 @@ import 'package:crypto/crypto.dart'; import 'package:encrypt/encrypt.dart' as encrypt; import 'package:tuple/tuple.dart'; -String encryptAESCryptoJS(String plainText, String passphrase) { +String encryptAESCryptoJS(Uint8List plainText, String passphrase) { try { final salt = genRandomWithNonZero(8); var keyndIV = deriveKeyAndIV(passphrase, salt); @@ -14,7 +14,7 @@ String encryptAESCryptoJS(String plainText, String passphrase) { final iv = encrypt.IV(keyndIV.item2); final encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc, padding: "PKCS7")); - final encrypted = encrypter.encrypt(plainText, iv: iv); + final encrypted = encrypter.encryptBytes(plainText, iv: iv); Uint8List encryptedBytesWithSalt = Uint8List.fromList(createUint8ListFromString("Salted__") + salt + encrypted.bytes); return base64.encode(encryptedBytesWithSalt); @@ -23,7 +23,7 @@ String encryptAESCryptoJS(String plainText, String passphrase) { } } -String decryptAESCryptoJS(String encrypted, String passphrase) { +List decryptAESCryptoJS(String encrypted, String passphrase) { try { Uint8List encryptedBytesWithSalt = base64.decode(encrypted); @@ -34,7 +34,7 @@ String decryptAESCryptoJS(String encrypted, String passphrase) { final iv = encrypt.IV(keyndIV.item2); final encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc, padding: "PKCS7")); - final decrypted = encrypter.decrypt64(base64.encode(encryptedBytes), iv: iv); + final decrypted = encrypter.decryptBytes(encrypt.Encrypted(encryptedBytes), iv: iv); return decrypted; } catch (error) { rethrow;