arrow-left

Only this pageAll pages
gitbookPowered by GitBook
1 of 14

BlueBubbles Clients

Loading...

Loading...

Loading...

Loading...

Loading...

Usage Guides

Loading...

Loading...

Loading...

Loading...

For Developers

Loading...

Loading...

Loading...

Licenses / Legal

hashtag
Disclaimers

  • This app is in no way affiliated with or endorsed by Apple and / or their affiliate(s), it provided is for educational purposes only. Apple, macOS, and iMessage are trademarks of Apple Inc., registered in the U.S. and other countries.

hashtag
Licenses

  • Tools and some image resources are under the .

  • 3rd party library licenses can be found within the app, under settings > about > licenses.

Intro and Background

Info on how the clients were coded, what features they have, how to contribute, and more.

hashtag
Background

All clients are coded in Flutter and Dart. Our first stable release was published June 15th, 2021, after extensive closed alpha and public beta testing since October of 2020. Along this journey we have learned quite a bit about development in Flutter, and we'd like to share that, along with other things, in the form of this documentation.

Apache license, version 2.0arrow-up-right
hashtag
Why Flutter?

We were choosing between Flutter and React Native to be our framework for the project. Initially, we ended up going with React Native because most of the developers were familiar with JavaScript and React development.

However, React proved to be difficult for us when creating the UI. We wanted our app to look like iMessage, but with a bit of our own twist - this meant a lot of blur, opacity, and bouncy scrolling. At the time, React did not have great support for these types of things, while with Flutter, it was as simple as providing decoration to a Container, or different ScrollPhysics to a scrollable widget.

We did not consider native development because we figured that development would be slow and clunky, even if the app might be more performant and memory efficient - the tradeoff would not be worth it.

hashtag
Features

  • Send & receive texts, media, and location

  • View tapbacks/reactions, stickers, and replies

  • Create new chats (macOS 11+ has limited support while macOS 10 has full support)

  • View read/delivered timestamps

  • Mute or archive conversations

  • Robust theming engine

  • Choose between an iOS or Android-style interface

  • Lots of customizations and options to personalize your experience

hashtag
Private API Features

  • Send and receive typing indicators

  • Mark chats read on the host macOS device

  • Create chats

  • Rename group chats

  • Add or remove participants from group chats

  • Send messages, replies, message effects, mentions, and tapbacks

hashtag
Supported Devices and Platforms

hashtag
Android

Android 5.0+

Android Auto (GitHub build required at this time)

WearOS (currently in alpha)

hashtag
Desktop

Windows 10 / 11 & Linux

hashtag
Web

Any modern browser

hashtag
Downloads

hashtag
Screenshots

Chat List
Message View
Private API Features
image.png
image.png
image.png

hashtag
Contributors

hashtag
Main Developers

hashtag
Other Contributors

We appreciate y'all!

Tasker Integration

Details on how BlueBubbles integrates with Tasker

circle-exclamation

Tasker integration requires BlueBubbles App v1.12.0 or greater!

circle-info

If you make any cool integrations, feel free to share in our Discord!

Using BlueBubbles with Adguard

This page will show you how you can use BlueBubbles with Adguard, if you are having connection issues.

hashtag
Overview

Adguard is a local VPN on your Android device that you can use for simple DNS blocking.

Many of you are using one of BlueBubble's built in proxy services such as Cloudflare or Ngrok. Sometimes, Adguard will block these domains, and can cause connection issues between your BlueBubbles Android app and the BlueBubbles Server.

Supporting the Project

How can you support the project?

Donate Herearrow-up-right

If you're even looking at this page, thank you! We appreciate any donations made to this effort, large or small! Any donations made will go directly to the main developers, split evenly. If more developers choose to contribute, we will happily split the donations with them.

Our developers have put hundreds of hours into the research, design, and creation of the BlueBubbles App ecosystem. All donations will help compensate the developers for their time. All developers are doing this as a side project, in their free time.

hashtag
Fetching the Server URL

To fetch the server URL in Tasker, follow the below process:

  1. Create the Send Intent task

    1. Set the action as com.bluebubbles.external.GET_SERVER_URL

    2. Set one extra as password:<your server password>. This is required so that apps cannot abuse this ability without your consent.

    3. Set the package as com.bluebubbles.messaging.

    4. Ensure the target is set as Broadcast Receiver.

  2. Create the Intent Received event

    1. Set the action as net.dinglisch.android.taskerm.BB_SERVER_URL

hashtag
Listening for Server Events

To listen for server events (new message, chat read status change, etc), follow the below process:

  1. Enable the option within BlueBubbles app settings > Tasker Integration

  2. Create the Intent Received intent

    1. Set the action as net.dinglisch.android.taskerm.BB_EVENT

    2. Create your own task to perform once this is received.

The intent sends a few pieces of data:

  1. The server URL, which can be accessed via the %url variable

  2. The type of event, which can be accessed via the %event variable

  3. The event data, which can be accessed via the %data variable

The event type is defined by the server event types, which can be found herearrow-up-right.

The event data is a JSON string. You can use Tasker's built-in JSON parser to convert this to a real JSON object and access the individual data inside.

hashtag
The Fix

The fix is simple. Just open the Adguard app's settings, and disable Adguard for the BlueBubbles App.

See the screenshot below for an example of this (note the Adguard protection toggle is disabled):

image.png
image.png
image.png

Desktop App Installation

How to install the desktop app from Github Releases

hashtag
Windows Users

hashtag
Microsoft Store

Automatic Updates for GitHub Android Releases

If you are using GitHub releases for one reason or another, whether it's because of Android Auto support or something else, follow this guide to receive automatic updates.

  1. Install the Obtainium app found

  2. Open the app

Create your own task to perform once this is received. The server URL can be accessed via the %url variable.
Tap on the Add App tab
  • In the 2nd field labelled, "Search", enter bluebubbles-app and click the "Search" button

  • Select the first result labelled, BlueBubblesApp/bluebubbles-app and then confirm using the "Pick" button

  • Set any custom configuration/filtering options you may want

  • Lastly, confirm/save the configuration using the "Add" button at the top

  • circle-info

    Before installing updates, make sure to confirm the version you are updating to, making sure it is newer.

    herearrow-up-right
    hashtag
    Standalone Executable (Windows)
    1. Download bluebubbles_standalone.exe from GitHub releases (linked above)

    2. Run the installer

    3. You may receive a warning from Windows Defender about an unrecognized publisher, simply click "More Info", and then "Run anyways" at the bottom.

    4. Complete the installation

    hashtag
    Linux Users

    hashtag
    Flatpak

    hashtag
    Standalone Executable (Linux)

    1. Download bluebubbles_linux.zip from GitHub Releases (linked above)

    2. Unzip bluebubbles_linux.zip

    3. Move libobjectbox.so from bundle/lib in bundle to /usr/lib

    4. Run the executable bluebubbles_app inside bundle

    Note: You might need to install the following dependencies:

    For notifications - libnotify-dev

    For video playback - vlc, libvlc-dev

    For the system tray - appindicator3-0.1, libappindicator3-dev

    You can use the following command:

    Build Yourself / Contribution Guide

    How to build the app on your own system, or how to contribute to the project

    circle-info

    This guide is tailored towards the Android Studio IDE. You can also build in VSCode, but some tools may not exist or may work differently.

    hashtag
    Pre-Requisites

    • Install , following the guide for your OS all the way up to the "Test Drive" page

    hashtag
    Build Instructions

    hashtag
    Initial Steps

    1. Clone the repository to your system

      1. We recommend building off the development branch! It has the more recent code that is more likely to build without errors.

    2. Add a file named .env

    hashtag
    Android

    1. Find android > app > build.gradle (not to be confused with android > build.gradle), and scroll down to signingConfig signingConfigs.release at line 111. Change this to signingConfig signingConfigs.debug.

    2. In a terminal window at the root of the project directory, run flutter build apk --release --flavor prod

    hashtag
    Web

    1. In /lib/repository/models/html add a new file called giphy.dart. Inside this, put only const GIPHY_API_KEY = "";. If you have your own API key for GIPHY, you can place it inside the quotes, otherwise leave it as is.

    2. In the terminal window at the root of the project directory, run flutter build web --web-renderer=canvaskit. It will output the build files to build/web

    hashtag
    Desktop

    hashtag
    Windows Build

    1. Install NuGet package manager

    2. Go to Visual Studio Installer -> Modify Build Tools -> Individual Components and install the latest Windows 10 SDK

    3. Run the following commands:

    hashtag
    Linux Build

    Under construction...

    hashtag
    Contribution Guide

    Hey there! We're glad you want to contribute. We only ask for these three things:

    1. Write clean code, and comment it!

    2. Follow Flutter & Dart best practices

    3. Avoid making large formatting changes to files, unless that is the goal of your PR. It makes it easier for us to review the changes this way.

    hashtag
    Get Started

    Make sure you've completed .

    1. Fork the repo, and then clone the fork to your system

    2. Open the files in the IDE of your choice

    3. Complete step 2 of

    circle-exclamation

    The client apps have a lot of variables that need to be tested. For example, if you're making a UI change, please make sure it looks good in all the default themes, and all the skins as well.

    If you wish to make a backend change, we suggest you consult with the main developers before writing code. This is so we can come up with a plan of attack and make sure we don't degrade existing functionality or create bugs.

    circle-info

    Please create all PRs targeting the development branch, not the master branch.

    circle-info

    If you're new to Flutter development, look out for the good-first-issue or the Difficulty: Easy label on GitHub. These will be easier issues to help you start learning Flutter, without dealing with an overly-complex bug or feature.

    Using Unified Push for Notifications

    This page will show you how you can set up Unified Push to receive notifications so you don't have to use Google Firebase or the Background Service

    Unified Push is an open-source protocol that enables users to choose how they receive push notifications, breaking the dependency on proprietary services. It empowers users by giving them control over their notifications, promoting privacy, and allowing for a more decentralized and customizable notification experience.

    Unified Push can be self-hosted, or can be used with third-party "distributors". You can learn more about Unified Push and common distributors here:

    Once Unified Push is set up, you can configure your BlueBubbles Server with a new Webhook to send notifications/messages to your device.

    hashtag

    sudo apt-get -y install libnotify-dev vlc libvlc-dev appindicator3-0.1 libappindicator3-dev
    to the root of the project directory. Inside it, place
    GIPHY_API_KEY = ""
    . This is to prevent a build error.
  • You may need to accept licenses and perform other tasks since you are building Flutter for the first time. The terminal output will guide you through this process.

  • The output APK file path will be given to you, simply transfer it to your phone and install it!

  • to be hosted on your server.
    Make your changes
  • Run the app using flutter run or the green play button at the top of your IDE

  • Test your changes

  • Commit and PR!

    Make sure you don't commit your changes to comment out onContentCommit.

  • Flutterarrow-up-right
    Pre-Requisites
    Initial Steps
    flutter clean
    git reset
    flutter build windows
    Recommended Set Up

    Since Unified Push is essentially just a protocol, there can and will be many distributors that support it. You just have to choose one that works best for you. In our recommended setup, we will be using ntfy.sh as our provider. It is a free and easy to use service that can be used directly with the BlueBubbles App (v1.15.0+).

    1. Ensure you have the BlueBubbles App v1.15.0 or newer installed

    2. Install the ntfy.sh app

      1. Google Play: https://play.google.com/store/apps/details?id=io.heckel.ntfyarrow-up-right

      2. F-Droid:

    3. Open the nfty.sh app and allow notification permissions

    4. Open the Settings page in the BlueBubbles App

    5. Navigate to the Notification Providers settings page and open the Unified Push configuration.

    6. Enable Unified Push

      1. Within a few seconds, you should see an ntfy.sh URL generated below the toggle

    7. Switch to your BlueBubbles Server (Mac)

    8. Open the API & Webhooks tab

    9. Click Manage -> Add New Webhook

    10. Copy/Paste the URL provided in the BlueBubbles App into the new webhook popup

    11. Enable the webhook to for All Events

    12. Save the Webhook

    Now, when you are not actively using the BlueBubbles App, you should still receive notifications via Unified Push + ntfy.sh

    Flutter

    This section aims to provide extensive documentation and how-tos for all of the complex things we have managed to implement while working within the constraints of a Flutter app. We hope that this can be a useful guide / tool for other developers working on Flutter apps, especially Flutter-based chat apps.

    Check the sidebar for all the documentation! (WIP)

    hashtag
    Flutter "Review"

    As mentioned in Why Flutter?, we chose Flutter over building something in React Native, or in Java itself. When deciding our framework, we never had full cross platform in mind, and rather we were focusing on the ease of development. While native Android would definitely provide the best overall user experience, we decided to make a small tradeoff on this aspect to utilize Flutter's ease of use, scalability, and quick turnaround times.

    hashtag
    What We Love

    By far and away, the best thing about Flutter has to be the ease of development. Flutter layout design and UI is ridiculously easy to understand, and creating great UI is a simple task. We also really like Dart syntax, it makes code very readable and has a lot of great features to make coding easier.

    Pub has been a great package repo - There are so many high quality packages available to drop into your project and eliminate the need for a ton of boilerplate code. We use everything from small UI packages to larger state / framework / dependency management packages.

    There is definitely much more to like, but it isn't important to list all that here.

    hashtag
    What We Don't Like

    We would like to preface this section by saying we are not the most experienced developers in Flutter. Many of the issues listed here might be resolved with advanced code, but so far we haven't been able to find workarounds for them.

    One of the biggest issues we face with Flutter is performance and memory usage. Before Flutter 2.8, our app would often surpass 2 GB memory when loading up more than 10-15 JPEGs or MP4s. This functionality is crucial for a messaging app where multimedia is a high priority. We also had many issues with lag in our transitions, animations, and system events such as the keyboard going up and down. Most of the performance issues were resolved with painstaking optimizations in code and the Flutter 2.8 upgrade, however.

    Another large issue in Flutter is text input. Out of the box, Flutter doesn't support any sort of image / multimedia paste into textfields. This also means users can't insert GIFs or screenshots from Gboard on Android. To solve this, we ended up creating a fork of the Flutter SDK and engine, which we discuss in more detail at . For Web, we ended up using a Dart / JS interop function which is also discussed in the same section.

    Our final major downside is just how Flutter interfaces with native. Things like creating notifications, supporting features such as conversation bubbles, or using the default copy/paste menu, are difficult to implement or simply not possible at this time. Many of these issues have open tickets on GitHub but they are either P3, P4, or P5 which doesn't bode well for having any chance these features are added in the near future.

    hashtag
    Conclusion

    With all that said, we believe the pros vastly outweigh the cons. Flutter has been a godsend for creating beautiful UI easily, and Dart is a very easy language with great tools to create mobile apps. The package support is also excellent, we love being able to just drop in packages and quickly add new features using them. Above all else, Flutter allowed us to extend not only to mobile, but also Web and Desktop with very minimal changes to our initial mobile-only codebase.

    If you are creating a chat app specifically, keep the cons in mind. Be prepared to have to jump through hoops to integrate some important functionalities - however in the end it will be worth it.

    Image Insertion
    https://f-droid.org/en/packages/io.heckel.ntfy/arrow-up-right

    Image Insertion

    How we added the ability to insert images via keyboard or copy/paste into Flutter

    hashtag
    Android

    One of the most crucial requirements for a chat app is to allow users to insert rich content directly from their keyboard. On Android, this can be things such as stickers, GIFs, bitmoji, and even AI-based context-aware items (e.g. the keyboard offering to paste a recent screenshot). This functionality is sorely missing in Flutter, and we were forced to modify the Flutter Engine and Framework ourselves to implement it.

    The basic content commit API is described at https://developer.android.com/guide/topics/text/image-keyboardarrow-up-right. The Java code required to interface with Flutter's `TextInputClient` was added in the Flutter Engine.

    Our PR to the Flutter Engine has been merged! The code essentially attaches the TextInputChannel to the contentCommit API, and sends the file data through the PlatformChannel back to the Flutter Framework.

    Our PR to the Flutter Framework is currently pending review. The framework code organizes the data received from the PlatformChannel into a Dart class, and then this data can be manipulated however the developer chooses.

    hashtag
    Web

    Flutter currently does not have any way to detect image content when a paste action is performed on a text field. We implemented this with the great interoperability that Dart and JS have. We created a simple function that uses the JS package to call JS code from Dart:

    This is called when we detect a CTRL-V event on the text field using a RawKeyboardListener. The JS function unfortunately only works in Chrome, but it uses the browser Clipboard APIs to get any media content:

    This function is placed inside the web/index.html file as a <script>.

    hashtag
    Desktop

    We use the pasteboard package on Desktop to get the most recent item (image) from the clipboard. This is also called when we detect a CTRL-V event on the text field using a RawKeyboardListener.

    BlueBubbles - Apps on Google PlayGooglePlaychevron-right
    All Android stable and beta builds can be found on the Play Store
    Logo
    jjoelj - OverviewGitHubchevron-right
    Joel
    @JS()
    import 'package:js/js.dart';
    import 'package:js/js_util.dart';
    
    // Call invokes JavaScript `getPastedImage()`.
    @JS('getPastedImage')
    external dynamic getClipboardData();
    
    Future<dynamic> getPastedImageWeb() {
      return promiseToFuture(getClipboardData());
    }
    function checkClipboard() {
      return new Promise(resolve => {
        navigator.permissions.query({ name: "clipboard-read" }).then((result) => {
          // If permission to read the clipboard is granted or if the user will
          // be prompted to allow it, we proceed.
          if (result.state == "granted" || result.state == "prompt") {
            navigator.clipboard.read().then((data) => {
              for (let i = 0; i < data.length; i++) {
                if (!data[i].types.includes("image/png")) {
                  resolve(null);
                } else {
                  data[i].getType("image/png").then((blob) => {
                    resolve(blob);
                  });
                }
              }
            });
          } else {
            resolve(null);
          }
        });
      });
    }
    
    async function getPastedImage() {
      const result = await checkClipboard();
      return result;
    }
    tneotia - OverviewGitHubchevron-right
    Tanay
    zlshames - OverviewGitHubchevron-right
    Zach

    ObjectBox

    What we've learned and had to deal with while using ObjectBox as our database of choice

    hashtag
    Intro

    Initially, our app used the sqflitearrow-up-right package as our database (i.e. SQLite). The iMessage database on macOS is an SQLite database, so it made the most sense for our app to emulate the same structure (which it did, we copied the CREATE statements for each table and used them to build our own internal database).

    This worked well for the most part, but we hit a major roadblock when we decided to port our app to desktop platforms using Flutter and the sqflite_common_ffiarrow-up-right package - The speeds decreased dramatically. A simple DB call could take upwards of 5-10 seconds, and this was causing significant delay for actions like loading chats, message threads, etc which should normally take up to 1 second.

    As a result, we decided to look into alternative databases. We landed on two potential ones: and . We first tried sqlite3 to avoid a huge migration of code, but it was the same issue - speeds were too slow on desktop. Then, we moved to objectbox. The speed issue was resolved, but we discovered a whole host of other issues that required some hacky workarounds and advanced Dart coding to resolve, and this page will discuss that in depth.

    hashtag
    Web Support

    It was very tricky to get our code to compile for web while using ObjectBox. The issue is that it uses some dart:io exclusive code, such as Pointers and other things.

    As a result, we need to create two separate files for the same model, with very slightly different code. Here's a quick example:

    chevron-right/io/model.darthashtag
    chevron-right/web/model.darthashtag

    When importing this model into other files, you want to use the following import directive:

    This ensures that when compiling for web platforms, it does not import ObjectBox libraries, which would prevent the app from compiling.

    The same process can be used for any functions that call ObjectBox code, create an io version which has the function actually perform a task, and the web version is empty:

    chevron-right/io/functions.darthashtag
    chevron-right/web/functions.darthashtag

    Then use a similar import directive as above, and if you wish, you can call this function on io only by using if (!kIsWeb) openObjectbox(); in your code.

    hashtag
    Asynchronous API

    The other big issue with ObjectBox is a lack of an asynchronous API. If you use synchronous code for larger reads in your app, it could cause heavy jittering and lag on page load. This was a huge problem for us, but thankfully there are two ways to mitigate it.

    hashtag
    Using Dart Isolates

    circle-exclamation

    Requires Flutter 2.5 (Dart 2.14) and higher!

    triangle-exclamation

    Do not use this method if you plan to use ObjectBox Relations! It will not work!

    The first thing to do is store a reference to the ObjectBox Store object by encoding it into a base64 string:

    You'll want to do this right after you initialize your Store in the app.

    Next, let's take an example class:

    As you can see, we are trying to get chats from ObjectBox using a given limit and offset. We use the Flutter compute function which handles all the boilerplate code for creating an isolate, sending, and receiving arguments.

    getChatsIsolate must be a top level function, and is defined like so:

    We get the arguments from the list sent to the function. The most important part is to use the same Store reference as in the main thread!

    This code will get you the best performance possible in the app, and we highly recommend doing it this way. What if you must absolutely use relations though?

    hashtag
    Using async_task Package

    circle-info

    We only recommend using this method if you absolutely need to use ObjectBox Relations! This method is not as performant, but is still definitely better than using the synchronous API.

    When using the async_task package, the example shown above would look something like this:

    And GetChatAttachments is defined as follows:

    Finally, we also must add the createAsyncTask function:

    The parallelism: 0 is very important here. It makes sure the async_task package doesn't try to use an isolate (which would break due to the Relations), and instead use an async zone to run the task.

    hashtag
    Tricks to Eliminate Jank

    Oftentimes jank will happen on animations - you will see dropped frames or stutters. To solve this, there are some things you should check and try doing:

    • Use transactions to group larger reads / writes into a single block

    • Reduce the number of reads / writes by simplifying your code

    • Run database code after page animations are complete:

    hashtag
    Using ObjectBox inside a Background Isolate

    You may have a background isolate that runs Dart code when an external event happens, e.g. when you receive an FCM notification. This is the proper way to use the Store inside your isolate:

    Every time you initialize a new Store, you should store the reference inside some sort of SharedPreferences, like so:

    This ensures that your code retains concurrency if your app happens to be active but also receives a background event that starts the secondary isolate.

    sqlite3arrow-up-right
    objectboxarrow-up-right
    import 'package:objectbox/objectbox.dart';
    
    @Entity()
    class ScheduledMessage {
      int? id;
      String? chatGuid;
      String? message;
      int? epochTime;
      bool? completed;
    
      ScheduledMessage({this.id, this.chatGuid, this.message, this.epochTime, this
    }
    class ScheduledMessage {
      int? id;
      String? chatGuid;
      String? message;
      int? epochTime;
      bool? completed;
    
      ScheduledMessage({this.id, this.chatGuid, this.message, this.epochTime, this
    }
    export 'package:path/to/io/scheduled.dart'
        if (dart.library.html) 'package:path/to/html/scheduled.dart';
    import 'package:objectbox/objectbox.dart';
    
    void openObjectbox() {
        store = await openStore(directory: '/objectbox');
        ...
    }
    void openObjectbox() {}
    prefs.setString("objectbox-reference", base64.encode(store.reference.buffer.asUint8List()));
    @Entity()
    class Chat {
       //other parameters here
    
      static Future<List<Chat>> getChats({int limit = 15, int offset = 0}) async {
         return await compute(getChatsIsolate, [limit, offset, prefs.getString("objectbox-reference")!, fakeNames]);
      }
    }
    /// Async method to get chats from objectbox
    Future<List<Chat>> getChatsIsolate(List<dynamic> stuff) async {
      /// Pull args from input and create new instances of store and boxes
      store = Store.fromReference(getObjectBoxModel(), base64.decode(stuff[2]).buffer.asByteData());
      attachmentBox = store.box<Attachment>();
      chatBox = store.box<Chat>();
      handleBox = store.box<Handle>();
      messageBox = store.box<Message>();
      return store.runInTransaction(TxMode.read, () {
        /// Query the [chatBox] for chats with limit and offset, prioritize pinned
        /// chats and order by latest message date
        final query = (chatBox.query()
              ..order(Chat_.isPinned, flags: Order.descending)
              ..order(Chat_.latestMessageDate, flags: Order.descending))
            .build();
        query
          ..limit = stuff[0]
          ..offset = stuff[1];
        final chats = query.find();
        query.close();
        return chats;
      });
    }
    @Entity()
    class Chat {
       //other parameters here
    
      static Future<List<Chat>> getChats({int limit = 15, int offset = 0}) async {
         final task = GetChatAttachments([id!, prefs.getString("objectbox-reference")]);
         return (await createAsyncTask<List<Attachment>>(task)) ?? [];
      }
    }
    /// Async method to get chats from objectbox
    class GetChats extends AsyncTask<List<dynamic>, List<Chat>> {
      final List<dynamic> stuff;
    
      GetChats(this.stuff);
    
      @override
      AsyncTask<List<dynamic>, List<Chat>> instantiate(List<dynamic> parameters, [Map<String, SharedData>? sharedData]) {
        return GetChats(parameters);
      }
    
      @override
      List<dynamic> parameters() {
        return stuff;
      }
    
      @override
      FutureOr<List<Chat>> run() {
        return store.runInTransaction(TxMode.read, () {
          /// Query the [chatBox] for chats with limit and offset, prioritize pinned
          /// chats and order by latest message date
          final query = (chatBox.query()
            ..order(Chat_.isPinned, flags: Order.descending)
            ..order(Chat_.latestMessageDate, flags: Order.descending))
              .build();
          query
            ..limit = stuff[0]
            ..offset = stuff[1];
          final chats = query.find();
          query.close();
          return chats;
        });
      }
    }
    /// Create a "fake" asynchronous task from a traditionally synchronous task
    ///
    /// Used for heavy ObjectBox read/writes to avoid causing jank
    Future<T?> createAsyncTask<T>(AsyncTask<List<dynamic>, T> task) async {
      final executor = AsyncExecutor(parallelism: 0, taskTypeRegister: () => [task]);
      executor.logger.enabled = true;
      executor.logger.enabledExecution = true;
      await executor.execute(task);
      return task.result;
    }
    //run inside initState
    WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
      if (ModalRoute.of(context)?.animation != null) {
        if (ModalRoute.of(context)?.animation?.status != AnimationStatus.completed) {
          late final AnimationStatusListener listener;
          listener = (AnimationStatus status) {
            if (status == AnimationStatus.completed) {
              fetchAttachments();
              ModalRoute.of(context)?.animation?.removeStatusListener(listener);
            }
          };
          ModalRoute.of(context)?.animation?.addStatusListener(listener);
        } else {
          fetchAttachments();
        }
      } else {
        fetchAttachments();
      }
    });
    String? storeRef = prefs.getString("objectbox-reference");
    if (storeRef != null) {
      debugPrint("Opening ObjectBox store from reference");
      try {
        store = Store.fromReference(getObjectBoxModel(), base64.decode(storeRef).buffer.asByteData());
      } catch (_) {
        debugPrint("Failed to open store from reference, opening from path");
        store = await openStore(directory: documentsDirectory.path + '/objectbox');
      }
    } else {
      debugPrint("Opening ObjectBox store from path");
      store = await openStore(directory: documentsDirectory.path + '/objectbox');
    }
    prefs.setString("objectbox-reference", base64.encode(store.reference.buffer.asUint8List()));
    .
    completed})
    ;
    .
    completed})
    ;
    Logo
    Logo
    Join the BlueBubbles Discord Server!Discordchevron-right
    Alpha builds can be found in Discord
    elliotnash - OverviewGitHubchevron-right
    Elliot
    Logo
    Bricktheworld - OverviewGitHubchevron-right
    Brick
    BlueBubbles - Free download and install on Windows | Microsoft StoreMicrosoft Store - Download apps, games & more for your Windows PCchevron-right
    All Windows stable and ebta builds can be found on the Microsoft Store
    Releases · BlueBubblesApp/bluebubbles-appGitHubchevron-right
    Github Releases
    BlueBubbles - Free download and install on Windows | Microsoft StoreMicrosoft Store - Download apps, games & more for your Windows PCchevron-right
    Microsoft Store Download
    Logo
    SoPat712 - OverviewGitHubchevron-right
    Intellijence
    UnifiedPushUnifiedPushchevron-right
    Logo
    Logo
    Releases · BlueBubblesApp/bluebubbles-appGitHubchevron-right
    All client stable and beta builds can be found on GitHub
    Install BlueBubbles on Linux | Flathubflathub.orgchevron-right
    BlueBubbles on Flathub
    Image keyboard support · Issue #20796 · flutter/flutterGitHubchevron-right
    Relevant Flutter Issue
    Logo
    pasteboard | Flutter packageDart packageschevron-right
    Pasteboard Package
    Logo
    Logo
    Logo
    Add support for image insertion on Android by tneotia · Pull Request #35619 · flutter/engineGitHubchevron-right
    PR to Flutter Engine
    Logo
    Logo
    Logo
    Logo
    Logo
    Add support for image insertion on Android by tneotia · Pull Request #110052 · flutter/flutterGitHubchevron-right
    PR to Flutter Framework
    Logo
    Logo