Skip to content
Snippets Groups Projects
Select Git revision
  • ci-web-publishing
  • 470-duplicated-licenses
  • main default protected
  • 482-logout-file-deletion
  • 127-left-user-status
  • smoketest-rework
  • 383-several-bugs-in-addmemberstoroom-view
  • compose-beta-update
  • 451-consistent-behavior-for-file-download-on-android
  • add-errors-when-updating-room-avatars
  • ios-client
  • 342-rework-pdf-readers
  • styling-iconbutton-fix
  • 400-general-metadata-improvements
  • 375-group-name-topic-switch-crash-fix
  • 400-general-metadata-improvements-2
  • 220-configure-push-provider
  • 220-configure-push-provider-rework
  • sysnotify-integration
  • TM293-show-name-of-account-which-triggered-verification
  • v3.5.5
  • v3.5.4
  • v3.5.3
  • v3.5.2
  • v3.5.1
  • v3.5.0
  • v3.4.4
  • v3.4.3
  • v3.4.2
  • v3.4.1
  • v3.4.0
  • v3.3.0
  • v3.2.0
  • v3.1.1
  • v3.1.0
  • v3.0.3
  • v3.0.2
  • v3.0.1
  • v3.0.0
  • v2.4.0
40 results

trixnity-messenger

  • Clone with SSH
  • Clone with HTTPS
  • user avatar
    Benedict authored
    Fixed redacted naming.
    
    See merge request connect2x/trixnity-messenger/trixnity-messenger!97
    5e8f13d9
    History

    Trixnity Messenger - A headless Matrix messenger SDK

    Trixnity Messenger provides extensions on top of Trixnity geared towards building a multiplatform messenger. It is agnostic to the UI and supports all technologies that are interoperable with Kotlin. UI frameworks that are reactive like Compose Multiplatform, SwiftUI , or ReactJS are best suited, since the changes in Trixnity Messenger can be reflected in the component by binding to the view model.

    You need help? Ask your questions in #trixnity-messenger:imbitbu.de.

    TI-Messenger

    Are you looking for a TI-Messenger SDK? Trixnity Messenger is the base for our TIM SDK. If you want to know more, contact us at contact@connect2x.de.

    MVVM

    Trixnity Messenger follows the MVVM pattern, in particular it represents the view model (VM) part. Trixnity is the model (M), containing all Matrix-related logic. The view (V) on top of Trixnity Messenger is the presentation geared towards the user.

    This patterns frees the UI layer from lots of logic that can be complicated (and thus needs to be tested). In an ideal case the UI just consumes information provided by the view model and presents it. When user interaction occurs, the corresponding methods in the view model are called (which can lead to changes in the model and therefore the view model) .

    This is an overview on how different UI technologies can be used on top of trixnity-messenger:

    Getting Started

    First you need to add the maven repository:

    maven("https://gitlab.com/api/v4/projects/47538655/packages/maven")

    Now you are able to add trixnity-messenger as dependency to your project:

    implementation("de.connect2x:trixnity-messenger:<version>")

    Just create MatrixMessenger including the view model tree that is used in your app.

    val matrixMessenger = MatrixMessenger.create()

    Create a root node in your UI framework and pass the RootViewModel to it by calling matrixMessenger.createRoot(). In Compose Multiplatform on the desktop, it looks something like this:

    application {
        Window("My App") {
            MyMatrixClient(matrixMessenger.createRoot())
        }
    }

    where MyMatrixClient is a @Composable function that gets the RootViewModel as a parameter.

    Now you are ready to react to different states of the routing in the RootViewModel.

    Multi profiles/tenancy

    MatrixMessenger has support for multiple Matrix accounts, but by default it is not possible to have different MatrixMessenger instances in the same application (e.g., for a multi-tenancy feature).

    This feature is added by MatrixMultiMessenger (which inherits from ProfileManager). It allows to create, delete and select profiles. Profiles are used to separate MatrixMessengers . When you want to use a MatrixMessenger, you first decide on the profile and then have access to the current active MatrixMessenger. Unlike multiple Matrix accounts within MatrixMessenger, which are active at the same time, only one profile in MatrixMultiMessenger can be active at the same time. Under the hood, MatrixMultiMessenger creates another directory level on targets supporting filesystems or adds a prefix to all storage-keys on the web target. By default, the last active profile is loaded on start of MatrixMultiMessenger.

    Currently, it is not possible to migrate from MatrixMessenger to MatrixMultiMessenger, so we recommend to always use MatrixMultiMessenger and create a single profile on first start (for example by using the helper extension MatrixMultiMessenger.singleMode).

    Creating a MatrixMultiMessenger is similar to creating a MatrixMessenger:

    val matrixMultiMessenger = MatrixMultiMessenger.create()

    Routing

    The RootViewModel itself does not do much on its own, but is a point where routing kicks in. Different views in the view models are organized in stacks that show one view on top and possibly some views behind the top stack ( see Decompose routing).

    In our case, let's have a look at rootViewModel.rootStack. It returns a Value<ChildStack<RootRouter.Config, RootRouter.RootWrapper>>, i.e. a value changing over time that is providing the UI with an instance of RootRouter.RootWrapper. In a first step, let's observe this value and react to changes:

    // this code has to be called from a `suspend` function
    rootViewModel.rootStack.toFlow()
        .mapLatest { it.active.instance }
        .collect { wrapper ->
            when (wrapper) {
                is RootRouter.RootWrapper.None -> {} // draw an empty UI
                is RootRouter.RootWrapper.MatrixClientInitialization -> {} // show initialization of the MatrixClient (aka loading screen)
                else -> {} // add more cases    
            }
        }

    In case you are using Compose as your UI framework, Decompose has some helpers for routing.

    Routing overview

    To better understand how the routers are connected, the following (incomplete) overview might help. Many details are left out for clarity.

    Configuration

    Trixnity Messenger has multiple ways to configure the client to your needs.

    Change default configuration

    The class MatrixMessengerConfiguration contains information that is used to determine some folder names and other data in the lifecycle of the messenger. To override the standard configuration use MatrixMessenger.create:

    val matrixMessenger = MatrixMessenger.create {
        appName = "Dino Messenger"
        // ... more config ...
    }

    Change the default behavior of view models

    You can customize the messenger SDK to fit your needs with the help of dependency injection (DI). Trixnity Messenger uses Koin for this.

    Suppose you want to deliver a demo version of your messenger and with it, want to fix the server url when the client tries to login the user to a Matrix server. To do this, you have to do the following:

    • provide an alternative implementation to a view model interface, here AddMatrixAccountViewModel
      class MyAddMatrixAccountViewModel(
        viewModelContext: ViewModelContext,
        addMatrixAccountViewModel: AddMatrixAccountViewModelImpl,
    ) : ViewModelContext by viewModelContext, AddMatrixAccountViewModel by addMatrixAccountViewModel {
    
        private val isDemoVersion: Boolean = ... // this is computed from the config or a runtime parameter
        val canChangeServerUrl: Boolean = !isDemoVersion
        override val serverUrl: MutableStateFlow<String> =
            MutableStateFlow(if (isDemoVersion) "https://myUrl" else addMatrixAccountViewModel.serverUrl.value)
    }

    Then, we have to register the new view model in a module:

    fun addMatrixAccountModule() = module {
        single<AddMatrixAccountViewModelFactory> {
            object : AddMatrixAccountViewModelFactory {
                override fun create(
                    viewModelContext: ViewModelContext,
                    onAddMatrixAccountMethod: (AddMatrixAccountMethod) -> Unit,
                    onCancel: () -> Unit
                ): AddMatrixAccountViewModel {
                    return MyAddMatrixAccountViewModel(
                        viewModelContext,
                        AddMatrixAccountViewModelImpl(viewModelContext, onAddMatrixAccountMethod, onCancel),
                    )
                }
            }
        }
    }

    Finally, add it to the modules of MatrixMessenger. You should always extend the default modules from createDefaultTrixnityMessengerModules():

    val matrixMessenger = MatrixMessenger.create {
        modules += addMatrixAccountModule()
    }

    When you start your application with this configuration, the implementation of AddMatrixAccountViewModel will be your customized version MyAddMatrixAccountViewModel and your UI can use all the properties and methods of it (maybe a downcast from the AddMatrixViewModel interface is needed).

    i18n

    Trixnity Messenger comes with a set of standard translations for some states that can occur. It currently supports English (en) and German (de). It uses a simple Kotlin file for all translations.

    It allows the same customizations as view models. In order to change messages, simply override the messages you want to change by subclassing I18nBase.

    If you want to add new messages, use the delegation pattern as described in View model customization and add more messages.

    Drag and Drop

    To support Drag and Drop, you need to get DragAndDropHandler from the DI:

    val dragAndDropHandler = matrixMessenger.defaultDragAndDropHandler // helper extension
    // call functions on dragAndDropHandler

    URL / SSO

    To support URL handling, you need to get UrlHandler from the DI:

    val urlHandler = matrixMessenger.defaultUrlHandler // helper extension
    // call functions on urlHandler depending on the platform (only on Android, iOS, JVM)

    Export room

    When exporting a room (via view model or ExportRoom), a properties instance needs to be defined. Currently, the following types do exist by default:

    • PlainTextFileBasedExportRoomProperties
    • CSVFileBasedExportRoomProperties

    Custom file format (converter)

    The FileBasedRoomExportRoomSink can be extended by other file formats. This need three steps:

    1. Define properties inheriting from FileBasedExportRoomProperties.
    2. Implement an instance of FileBasedExportRoomConverter
    3. Define a factory extending FileBasedExportRoomSinkConverterFactory and put this into the DI (e.g. via singleOf(::CustomFactory).bind<FileBasedExportRoomSinkConverterFactory>()).

    For more details take a look at existing FileBasedExportRoomSinkConverter like CSVFileBasedExportRoomSinkConverter.

    Custom export

    It is possible to define a completely custom RoomExportSink to export a room to other targets then files. For example a REST endpoint. For this, a ExportRoomSinkFactory needs to be defined and put into the DI (e.g. via singleOf(::CustomFactory).bind<ExportRoomSinkFactory>()).

    Usage from Swift (iOS or Mac)

    Trixnity Messenger can also be consumed in Swift code to build native iOS or Mac applications.

    Installation

    At this moment, the pipeline for Swift builds has not been automated in the CI (this is on our todo-list). Download the current version here. You can import the XCFramework locally into your project.

    Initialization

    In order to use the library from Swift, always import trixnity_messenger in your files.

    To create an instance of Trixnity Messenger do the following:

    let matrixMessenger = MatrixMessenger.companion.create()

    Pass this view model to your root UI node, e.g., a view in SwiftUI:

    var body: some Scene {
        WindowGroup {
            RootView(matrixMessenger.createRoot())
        }
    }

    Values and Flows

    Trixnity Messengers provides many properties that can change over time. Two data types are used: Values and Flows ( with its specializations StateFlow and MutableStateFlow). To access those values and get informed when they update, different helpers can be used.

    Value

    Values represent the changes in the routers (the data type is coming from decompose).

    Use this code to get a Swift wrapper for Values.

    import Foundation
    import trixnity_messenger
    
    public class ObservableValue<T : AnyObject> : ObservableObject {
        private let observableValue: Value<T>
    
        @Published
        var value: T
    
        private var observer: ((T) -> Void)?
        
        init(_ value: Value<T>) {
            observableValue = value
            self.value = observableValue.value
            observer = { [weak self] value in self?.value = value }
            observableValue.subscribe(observer: observer!)
        }
    
        deinit {
            observableValue.unsubscribe(observer: self.observer!)
        }
    }
    import SwiftUI
    import trixnity_messenger
    
    @propertyWrapper struct StateValue<T : AnyObject>: DynamicProperty {
        @ObservedObject
        private var obj: ObservableValue<T>
    
        var wrappedValue: T { obj.value }
    
        init(_ value: Value<T>) {
            obj = ObservableValue(value)
        }
    }

    This allows for the following code to work in SwiftUI:

    struct RootView: View {
        @StateValue
        private var stack: ChildStack<RootRouter.Config, RootRouter.RootWrapper>
        private var activeStack: RootRouter.RootWrapper { stack.active.instance }
        
        init(_ viewModel: RootViewModel) {
            _stack = StateValue(viewModel.rootStack)
        }
        
        // ...
    }

    Now, you can access activeStack in your view and get the current router value all the time.

    Flows

    Trixnity Messenger uses SKIE to generate some helper code to get nicer interfaces of Flows when accessing them from Swift code. To make it even easier, you can use the following helper:

    func observe<T>(_ stateFlow: SkieSwiftStateFlow<T>, _ assign: (T) -> ()) async {
      for await value in stateFlow.map({$0}) {
          assign(value)
      }
    }

    For some primitive values (Bool, Int, etc.) you might want to add specializations of this method.

    To use flows from SwiftUI, create a wrapper of the view model you want to observe. As an example:

    class AddMatrixAccountViewModelSwift: ObservableObject {
        let delegate: AddMatrixAccountViewModel
        @Published private(set) var serverDiscoveryState: AddMatrixAccountViewModelServerDiscoveryState
        
        init(delegate: AddMatrixAccountViewModel) {
            self.delegate = delegate
            self.serverDiscoveryState = delegate.serverDiscoveryState.value
        }
        
        @MainActor
        func activate() async {
            await observe(delegate.serverDiscoveryState) { self.serverDiscoveryState = $0 }
        }
    }

    It can be initiated like this:

    struct AddMatrixAccountView: View {
     
        @ObservedObject private var viewModel: AddMatrixAccountViewModelSwift
        
        init(_ addMatrixAccountViewModel: AddMatrixAccountViewModel) {
            viewModel = AddMatrixAccountViewModelSwift(delegate: addMatrixAccountViewModel)
        }
        
        // here you can access `viewModel.serverDiscoveryState` and always get the last value
    }
    .task { // this is important: activate the wrapper view model observation of the original view model
        await viewModel.activate()
    }

    Root path

    On the JVM (not Android) the root path can be overridden by setting an environment variable named TRIXNITY_MESSENGER_ROOT_PATH.

    Snapshot builds

    Snapshot are published on each commit to main. Append -SNAPSHOT-COMMIT_SHORT_SHA to the current version. You can find the COMMIT_SHORT_SHA here. You may also add https://gitlab.com/api/v4/projects/26519650/packages/maven to your maven repositories, which contains SNAPSHOT versions of Trixnity.

    Upgrade lock

    If any dependency is upgraded, the locks also have to be upgraded. This is done with the following command:

    Run ./gradlew dependenciesForAll --write-locks --no-parallel.

    Contributions

    If you want to contribute to the project, you need to sign the Contributor License Agreement. See CLA_instructions.md for more instructions.

    Commercial license and support

    If you need a commercial license or support contact us at contact@connect2x.de.