Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6,105 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

UI

If you are just interested in the UI and white labelling it, have a look at this Readme.

iOS Client

Trixnity Messenger also has support for iOS devices. Warning: The implementation is partly incomplete and experimental. If bugs occur, please report them.

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

Trixnity Messenger is published on Maven Central:

implementation("de.connect2x.trixnity.messenger:trixnity-messenger:<version>") // SDK
implementation("de.connect2x.trixnity.messenger:trixnity-messenger-compose-view:<version>") // Compose View

If you want SNAPSHOT builds, you can inclide the following maven repository:

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

Create a 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.

You can always look into the example Compose app or Tammy as a reference for a Compose multiplatform messenger implementation of Trixnity Messenger.

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. There are three layers of configuration and dependency injection (DI).

Each configuration layer allows configuring the DI of that layer. Use moduleFactories to add custom modules to the DI.

The configurations contain information used to determine some folder names and other data in the lifecycle of the messenger.

Configurations are available when creating an instance of MatrixMultiMessenger or MatrixMessenger using create.

MatrixMultiMessengerConfiguration

This configuration allows configuring the MatrixMultiMessenger itself. Use messengerConfiguration to configure the MatrixMessengerConfiguration.

AppIcon

Trixnity Messenger supports platform-specific app icons. AppIcon is a simple interface that provides the raw bytes of the icon:

interface AppIcon {
    suspend fun readBytes(): ByteArray
}

To render an AppIcon in the UI (Compose), a Painter is required. This is handled by the GetAppIconPainter interface, which should be registered in the DI container:

interface GetAppIconPainter<I : AppIcon> {
    val type: KClass<I>

    @Composable fun getPainter(icon: I): Painter
}

This allows defining icons in a platform-independent way (e.g., in ViewModels) while leaving the actual rendering logic (e.g., from resources or bitmaps) to the UI layer.

Example: Custom AppIcon
  1. Define your platform-independent AppIcon implementation:

    data class MyCustomAppIcon(val resourceName: String) : AppIcon {
        override suspend fun readBytes(): ByteArray = ... // Load bytes if needed
    }
  2. Implement the corresponding GetAppIconPainter in your UI module:

    object GetMyCustomAppIconPainter : GetAppIconPainter<MyCustomAppIcon> {
        override val type = MyCustomAppIcon::class
    
        @Composable override fun getPainter(icon: MyCustomAppIcon): Painter {
            return painterResource(...) // Load actual painter
        }
    }
  3. Register it in your Koin module using the appIconPainter extension:

    val myModule = module {
        appIconPainter { GetMyCustomAppIconPainter }
    }

MatrixMessengerConfiguration

This configuration allows configuring the MatrixMessenger itself. When MatrixMultiMessengerConfiguration is already used, some of the configuration options don't need to be set again. See MatrixMessengerBaseConfiguration to find out, which options are copied. Use clientConfiguration to configure the MatrixClientConfiguration.

Automatic download limits

Use MatrixMessengerConfiguration.downloadLimits to restrict automatic download sizes and prevent resource exhaustion.

You can set limits for specific media types (image, thumbnail, video, audio, pdf) alongside the following general properties:

  • default: default/fallback value for any media type not explicitly configured or unknown
  • export: limit for each file during export of a room

MatrixClientConfiguration

This configuration allows configuring the MatrixClient itself.

Notifications

Trixnity Messenger can show system notifications using Sysnotify.

NotificationProvider

A NotificationProvider decides when notifications should be retrieved. Usually this would be a push service, but long polling would be also a szenario. There are a few default implementations:

  • NoOpNotificationProvider does nothing and can be used when no other provider is needed (usually on Desktop).
  • PushNotificationProvider can be inherited to implement a push based notification provider.
    • FcmPushNotificationProvider (dependency de.connect2x:trixnity-messenger-notification-fcm)
    • ApnPushNotificationProvider (dependency de.connect2x:trixnity-messenger-notification-apn)
    • UnifiedPushNotificationProvider (dependency de.connect2x:trixnity-messenger-notification-unifiedpush)

To set a notification provider, a DSL in MatrixMultiMessengerConfiguration can be used. For example:

addFcmPushNotificationProvider()

Fonts

Trixnity Messenger uses the System Font per default. This behavior can be changed by providing a custom implementation of ThemeTypography. One such custom implementation setting the font to Nunito can be found in trixnity-messenger-compose-view-typography-nunito.

To use Nunito instead of the System Font, a DSL in MatrixMultiMessengerConfiguration can be used similar to above.

addNunitoThemeTypography()

You can also follow the example and create your own instance of ThemeTypography with custom fonts.

Adjusting the log level

Currently the log level can be adjusted using environment variables and config files employed by the underlying logging implementation.

An exception to this being the Web client, where the log level may be adjusted using the ?loglevel=LEVEL query parameter, where LEVEL can be one of the constants described in Kotlin Logging's Level enum.

MatrixClientConfiguration

If you want to change the underlying MatrixClientConfiguration, you can register a ConfigureMatrixClientConfiguration in the DI:

single<ConfigureMatrixClientConfiguration>(named("myConfig")) { // don't forget to name the singleton
    ConfigureMatrixClientConfiguration {
        autoJoinUpgradedRooms = false
    }
}

Add HttpClientEngine

Although Ktors HttpClients used by Trixnity (Messenger) automatically use a HttpClientEngine defined in the classpath, it is highly recommended to explicitly set it in the configuration. Only that way, it can be shared between all HttpClient instances. Otherwise, each HttpClient creates a new HttpClientEngine, which can lead to performance issues on heavy usage of the SDK.

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 = false // TODO: compute from config or 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 {
    modulesFactories += ::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).

Settings

Trixnity Messenger reads and stores settings into a JSON representation. As default there are MatrixMessengerSettings and MatrixMultiMessengerSettings, which inherit from Settings. To get a type safe representation of Settings, there is interface SettingsView<S : Settings<S>>. This allows to define custom views of the Settings, which usually can be accessed via Kotlin extensions (for example settings.base).

Use settings

Settings are managed by MatrixMessengerSettingsHolder and MatrixMultiMessengerSettingsHolder. You can inject them via DI to read or update fields.

Extend settings

Settings can be extended with own fields.

@Serializable
@NestedSettingsView("custom") // optionally: allows to nest settings in the json under a given key
data class MatrixMessengerCustomSettings(
    val customField: String? = null
) : SettingsView<MatrixMessengerSettings>

val MatrixMessengerAccountSettings.custom
        by settingsView<MatrixMessengerSettings, MatrixMessengerCustomSettings>()

This can be read via settingsHolder.value.custom and updated via settingsHolder.update<MatrixMessengerCustomSettings>{ it.copy(customField="dino") }.

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)

On Android you need to call the following in your Activity to put the Activity into your DI:

matrixMultiMessenger.defaultActivityGetter { this@Activity }
// OR
matrixMessenger.defaultActivityGetter { this@Activity }

Custom events

Adding custom events can be done via DI.

First create the custom event type:

@Serializable
data class CatEventContent(
    val hasEaten: Boolean,
    val isTired: Boolean,
) : MessageEventContent {
    // just ignore these properties when you don't need them
    override val relatesTo: RelatesTo? = null
    override val mentions: Mentions? = null
    override val externalUrl: String? = null
}

val catEventClientModule = module {
    single<CustomEventContentSerializerMappings>(named("CatEvent")) {
        CustomEventContentSerializerMappings {
            stateOf<CatEventContent>("de.connect2x.cat")
        }
    }
}

And add it to the client DI:

clientConfiguration {
    moduleFactories += ::catEventClientModule
}

Define the new view model:

interface CatMessageTimelineElementViewModelFactory : TimelineElementViewModelFactory<CatEventContent> {
    override fun create(
        viewModelContext: MatrixClientViewModelContext,
        content: CatEventContent,
        //...
    ): CatMessageTimelineElementViewModel? =
        CatMessageTimelineElementViewModelImpl(viewModelContext, content)

    override val supports: KClass<CatEventContent>
        get() = CatEventContent::class

    companion object : CatMessageTimelineElementViewModelFactory
}

interface CatMessageTimelineElementViewModel : MessageTimelineElementViewModel<CatEventContent> {
    val isPurring: Boolean
}

class CatMessageTimelineElementViewModelImpl(
    viewModelContext: MatrixClientViewModelContext,
    content: TextBased.Notice,
) : CatMessageTimelineElementViewModel, MatrixClientViewModelContext by viewModelContext {
    override val isPurring: Boolean = content.hasEaten && content.isTired
}

Define the UI (you may skip that when you don't use compose-view):

class CatMessageMessageTimelineElementView : TimelineElementView<CatMessageTimelineElementViewModel> {
    override val supports: KClass<CatMessageTimelineElementViewModel> =
        CatMessageTimelineElementViewModel::class

    @Composable
    override fun createInTimeline(
        holder: BaseTimelineElementHolderViewModel,
        element: CatMessageTimelineElementViewModel,
    ) {
        Text("isPurring=${element.isPurring}")
    }

    // ...
}

Next, add both to the messenger DI:

fun catEventMessengerModule() = modules {
    timelineElementViewModelFactory<CatMessageTimelineElementViewModelFactory> { CatMessageTimelineElementViewModelFactory }
    timelineElementView<CatMessageMessageTimelineElementView> { CatMessageMessageTimelineElementView() }
}

messengerConfiguration {
    moduleFactories += ::catEventMessengerModule
}

If your custom event should support a full screen details view when the user clicks/taps on it, you may also implement TimelineElementDetailsView and add it to the DI using timelineElementDetailsView<CatTimelineElementDetailsView> { CatTimelineElementDetailsView() }

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>()).

User Search

Currently, 2 searches are supported: the original search and the "new search" which is found in some class names of classes that had to be altered to support the new search. To de-activate the new search, inside a MatrixMessengerConfiguration:

features.apply {
    enableNewSearch = false
}

Hint: all customization options discussed here are for the new search.

The new search has a core search framework which can be extended to search for users in other places than the homeserver.

SDK

Searching users is done via the UserSearchViewModel. Set a searchTerm and a search is triggered. On its own, the UserSearchViewModel does not perform a search anywhere. You have to register SearchProviders first. A default is already registered: HomeserverSearchProvider, which uses the POST /_matrix/client/v3/user_directory/search API on the homeserver.

SearchProviders define how a search is executed, e.g., searching in an LDAP or a custom search API. The returned results have to conform to the interface UserSearchResult which can be enriched with other data (see HomeserverUserSearchResult).

In case your search provider does not support a full-fledged full-text search or allows granular searches, you might also want to define some ability to filter the search results with self-defined filters. Every SearchProvider can define those supportedFilters. Different SearchProviders can also share the same filter - the UserSearchViewModel merges them and presents them as one to the UI (providedFilters).

Example:

data class MySearchFilter(val value: String): SearchFilter {
    override val key = Key
    companion object Key: SearchFilter.Key<MySearchFilter>
    override val isDisabled = value.isBlank()
    override val displayValue = value
}

class MySearchProviderFactory(
    // args that can be provided by the DI
): SearchProviderFactory<MyUserSearchResult, UserSearchContext> {
    override fun create(account: UserId): MySearchProvider {
        // check whether the account is special and does or does not need the search provider 
        return MySearchProvider(
            // args
        )
    }
}

class MySearchProvider(
    // args
): SearchProvider<MyUserSearchResult, UserSearchContext> {
    // ...
    override val supportedFilters: List<SearchFilter.Key<*>> = listOf(MySearchFilter) 
    // ...
    override suspend fun search(
        searchTerm: String,
        filters: List<SearchFilter>,
        searchContext: UserSearchContext,
        coroutineScope: CoroutineScope,
    ): SearchProviderResult<MyUserSearchResult> {
        val mySearchFilter = filters.filterIsInstance<MySearchFilter>().firstOrNull() ?: MySearchFilter("")
        // do something with mySearchFilter
    }
}

To register the custom SearchProvider (inside the DI):

module {
    searchUserProviderFactory { MySearchProviderFactory(get()) }
}

For more details, consult the KDoc of SearchViewModel, SearchProvider, SearchProviderFactory and other interfaces linked to those.

UI

The UI for the search is defined like this:

*************************************************
* [TextField: searchTerm                      ] *
*                                               *
* (Homeserver ✔)  (MySearchProvider ✔︎)          *
*                                               *
* <Filters                                  ▼ > *
*  [TextField: MySearchFilter                ]  *
*  [TextField/Selection/Switch: more filters ]  *
*                                               *
* --------------------------------------------- *
*                                               *
* {HomeserverResult1                          } *
* {HomeserverResult2                          } *
* {HomeserverResult3                          } *
* {MySearchProviderResult1                    } *
* {HomeserverResult4                          } *
*************************************************

Most of the UI elements are hidden if there is only 1 SearchProvider, which is the default.

In case more SearchProviders are registered, the UI allows the user to enable or disable certain search providers (Chips below the search term text field). To register your toggle, use:

class MySearchProviderToggleView : SearchProviderToggleView<MySearchProvider> {
    override val supports: KClass<out MySearchProvider> = MySearchProvider::class

    @Composable
    override fun create(
        searchProvider: SearchProvider<*, *>,
        providerSearchCanBeEnabled: Boolean,
        enabled: Boolean,
        setEnabled: () -> Unit,
    ) {
        // your UI (should be ThemedFilterChip)
    }
}

// registration via DI
module {
    searchProviderToggleView { MySearchProviderToggleView() }
}

In case one of the registered SearchProviders defines any supportedFilters, a minimizable filter card is shown which includes all UI elements for filter values.

class MySearchFilterInputView : SearchFilterInputView<SearchFilter.Key<*>> {
    override val supports: KClass<out SearchFilter.Key<*>> = MySearchFilter.Key::class

    @Composable
    override fun create(userSearchViewModel: UserSearchViewModel, searchFilterKey: SearchFilter.Key<*>) {
        // get the current value from the view model
        val searchFilters by userSearchViewModel.searchFilters.collectAsState()
        val value = searchFilters.filterIsInstance<MySearchFilter>().firstOrNull() ?: MySearchFilter("")
        // your UI, e.g., a TextField
    }
}

// registration via DI
module {
    searchFilterInputView { MySearchFilterInputView() }
}

Beneath, the found results are displayed. The results of different providers are mixed by the framework and can have their own UI representation.

class MySearchResultView : SearchResultView<MyUserSearchResult> {
    override val supports: KClass<out MyUserSearchResult> = MyUserSearchResult::class

    @Composable
    override fun create(
        userSearchResult: MyUserSearchResult,
        showOrigin: Boolean,
        onClick: (MyUserSearchResult) -> Unit,
    ) {
        // your UI
    }
}

// register via DI
module {
    searchResultView { MySearchResultView() }
}

Worker

Doing work while the messenger is running can be a common use case. To do that, you can implement the Worker interface into the DI:

single<Worker>(named("MyWorker")) { // don't forget to name the singleton
    MatrixMessengerWorker {
        longRunningTask()
    }
}

Root path

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

Snapshot / Dev builds

Snapshot are published on each commit to main (usually after a merge request is approved and merged). Append -DEV-<increasing_number> to the current version. You can find the released versions here.

You may also add https://gitlab.com/api/v4/projects/26519650/packages/maven to your maven repositories, which contains DEV versions of Trixnity.

Local builds

If you want to use a locally built version of Trixnity Messenger, and you're having issues with running publishMavenToLocal, build the project using

./gradlew publishKotlinMultiplatformPublicationToMavenLocal

instead and optionally invoke tasks like publishAndroidReleasePublicationToMavenLocal, publishJvmPublicationToMavenLocal and publishJsPublicationToMavenLocal if needed. Same goes for macOS/iOS targets.

Usage from Swift (iOS or Mac)

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

Installation

You can add a dependency with Swift Package Manager: gitlab.com/connect2x/trixnity-messenger/spm.git.

The easiest way to get started is to look at the example at: https://gitlab.com/connect2x/trixnity-messenger/example-swiftui

Initialization

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

You have to create a MatrixMultiMessenger and a MatrixMessenger in order to create a RootViewModel. Take a look at the example on how to achieve this.

After the creation of the RootViewModel, you can use it inside SwiftUI Views.

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).

Please refer to the example-swiftui code (StateValue.swift and ObservableValue.swift).

These helpers allow for the following code to work in SwiftUI:

struct RootView: View {
    @StateValue
    private var stack: ChildStack<RootRouter.Config, RootRouter.Wrapper>
    private var activeStack: RootRouter.Wrapper {
        stack.active.instance
    }

    init(_ viewModel: RootViewModel) {
        _stack = StateValue(viewModel.stack)
    }

    var body: some View {
        Group {
            switch activeStack {
            case let addMatrixAccount as RootRouter.WrapperAddMatrixAccount:
                AddMatrixAccountView(addMatrixAccount.viewModel)
                    // more cases ...
            }
        }
    }
}

Flows

Trixnity Messenger uses SKIE to generate some helper code to get nicer interfaces of Flows when accessing them from Swift code.

It is possible to access Flows and its specializations in SwiftUI code. Most notably is the SwiftUI helper class Observing. E.g., it is used in the AddMatrixAccountView of the example:

var body: some View {
    // ...
    Observing(viewModel.serverDiscoveryState) { serverDiscoveryState in
        // ...
    }
}

Observing takes care of all changes happening in the Flow and propagates changes to SwiftUI so that the UI can react accordingly. No other translation is needed.

Usage as a Web app and inside an iframe

Notifications

  • if the origin of the parent window and the iframe containing the messenger are not the same, notifications will not work!
  • Unfortunately, there is no workaround here. The only clean solution would involve postMessage() from the iframe to the parent window and handling the notification there
    • this requires code changes to Trixnity-Messenger and the containing app!

Clipboard (Chrome only)

  • in the iframe HTML element, include the following attribute: allow="clipboard-read *; clipboard-write *"

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.

About

Kotlin Multiplatform messenger SDK for Matrix using Trixnity - this is a mirror of https://gitlab.com/connect2x/trixnity-messenger/trixnity-messenger

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Contributors

Languages