Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,13 @@ interface ConfigurationRepository : WorkspaceConfigRepository {
suspend fun hasFirstConfiguration(userId: String): Boolean

suspend fun setTutorialNotes(hasTutorials: Boolean, userId: String)

// Self-hosted backend URL
suspend fun saveSelfHostedBackendUrl(url: String, userId: String)

suspend fun loadSelfHostedBackendUrl(userId: String): String?

suspend fun listenForSelfHostedBackendUrl(userId: String): Flow<String?>

suspend fun clearSelfHostedBackendUrl(userId: String)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.writeopia.core.configuration.repository

import io.writeopia.app.sql.NotesConfiguration
import io.writeopia.app.sql.SelfHostedConfiguration
import io.writeopia.app.sql.WorkspaceConfiguration
import io.writeopia.common.utils.extensions.toBoolean
import io.writeopia.common.utils.extensions.toLong
Expand All @@ -9,6 +10,7 @@ import io.writeopia.sdk.models.sorting.OrderBy
import io.writeopia.sqldelight.dao.ConfigurationSqlDelightDao
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map

class ConfigurationSqlDelightRepository(
private val configurationSqlDelightDao: ConfigurationSqlDelightDao
Expand All @@ -17,6 +19,7 @@ class ConfigurationSqlDelightRepository(
private val _arrangementPref: MutableStateFlow<String> =
MutableStateFlow(NotesArrangement.GRID.type)
private val _orderPreference: MutableStateFlow<String> = MutableStateFlow(OrderBy.UPDATE.type)
private val _selfHostedBackendUrl: MutableStateFlow<String?> = MutableStateFlow(null)

override suspend fun saveDocumentArrangementPref(
arrangement: NotesArrangement,
Expand Down Expand Up @@ -98,11 +101,36 @@ class ConfigurationSqlDelightRepository(
configurationSqlDelightDao.setOnboarded()
}

override suspend fun saveSelfHostedBackendUrl(url: String, userId: String) {
configurationSqlDelightDao.saveSelfHostedConfiguration(
SelfHostedConfiguration(url = url, user_id = userId)
)
refreshSelfHostedBackendUrl(userId)
}

override suspend fun loadSelfHostedBackendUrl(userId: String): String? =
configurationSqlDelightDao.getSelfHostedByUserId(userId)?.url

override suspend fun listenForSelfHostedBackendUrl(userId: String): Flow<String?> {
refreshSelfHostedBackendUrl(userId)
return configurationSqlDelightDao.listenForSelfHostedConfigurationByUserId(userId)
.map { it?.url }
}

override suspend fun clearSelfHostedBackendUrl(userId: String) {
configurationSqlDelightDao.deleteSelfHostedConfiguration(userId)
refreshSelfHostedBackendUrl(userId)
}

private suspend fun refreshArrangementPref(userId: String) {
_arrangementPref.value = arrangementPref(userId)
}

private suspend fun refreshOrderPref(userId: String) {
_orderPreference.value = getOrderPreference(userId)
}

private suspend fun refreshSelfHostedBackendUrl(userId: String) {
_selfHostedBackendUrl.value = loadSelfHostedBackendUrl(userId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ class InMemoryConfigurationRepository private constructor() : ConfigurationRepos
private val arrangementPrefs = mutableMapOf<String, String>()
private val sortPrefs = mutableMapOf<String, String>()
private val workSpacePrefs = mutableMapOf<String, String>()
private val selfHostedBackendPrefs = mutableMapOf<String, String>()

private val arrangementPrefsState = MutableStateFlow(NotesArrangement.STAGGERED_GRID.type)
private val sortPrefsState = MutableStateFlow(OrderBy.UPDATE.type)
private val selfHostedBackendState = MutableStateFlow<String?>(null)

override suspend fun saveDocumentArrangementPref(
arrangement: NotesArrangement,
Expand Down Expand Up @@ -47,26 +49,36 @@ class InMemoryConfigurationRepository private constructor() : ConfigurationRepos
override suspend fun listenOrderPreference(userId: String): Flow<String> =
sortPrefsState.asStateFlow()

override suspend fun hasFirstConfiguration(userId: String): Boolean {
return false
override suspend fun saveSelfHostedBackendUrl(url: String, userId: String) {
selfHostedBackendPrefs[userId] = url
selfHostedBackendState.value = url
}

override suspend fun setTutorialNotes(hasTutorials: Boolean, userId: String) {
TODO("Not yet implemented")
override suspend fun loadSelfHostedBackendUrl(userId: String): String? =
selfHostedBackendPrefs[userId]

override suspend fun listenForSelfHostedBackendUrl(userId: String): Flow<String?> =
selfHostedBackendState.asStateFlow()

override suspend fun clearSelfHostedBackendUrl(userId: String) {
selfHostedBackendPrefs.remove(userId)
selfHostedBackendState.value = null
}

override suspend fun hasFirstConfiguration(userId: String): Boolean = false

override suspend fun setTutorialNotes(hasTutorials: Boolean, userId: String) {}

override suspend fun isOnboarded(): Boolean = true

override suspend fun setOnboarded() { }
override suspend fun setOnboarded() {}

companion object {
private var instance: InMemoryConfigurationRepository? = null

fun singleton(): InMemoryConfigurationRepository {
return instance ?: run {
instance = InMemoryConfigurationRepository()
instance!!
fun singleton(): InMemoryConfigurationRepository =
instance ?: InMemoryConfigurationRepository().also {
instance = it
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,28 @@ import io.writeopia.sdk.models.document.Document
import io.writeopia.sdk.serialization.data.DocumentApi
import io.writeopia.sdk.serialization.extensions.toApi
import io.writeopia.sdk.serialization.extensions.toModel
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.datetime.Instant

class DocumentsApi(private val client: HttpClient, private val baseUrl: String) {
class DocumentsApi(
private val client: HttpClient,
private val baseUrl: String,
private val selfHostedBackendManager: SelfHostedBackendManager? = null
) {

private suspend fun getEffectiveBaseUrl(): String {
// If there's a connected self-hosted backend, use that URL instead
val connectionState = selfHostedBackendManager?.connectionState?.firstOrNull()
return if (connectionState is SelfHostedConnectionState.Connected) {
connectionState.url
} else {
baseUrl
}
}

suspend fun getNewDocuments(folderId: String, lastSync: Instant): ResultData<List<Document>> {
val response = client.post("$baseUrl/api/document/folder/diff") {
val effectiveBaseUrl = getEffectiveBaseUrl()
val response = client.post("$effectiveBaseUrl/api/document/folder/diff") {
contentType(ContentType.Application.Json)
setBody(FolderDiffRequest(folderId, lastSync.toEpochMilliseconds()))
}
Expand All @@ -32,7 +48,8 @@ class DocumentsApi(private val client: HttpClient, private val baseUrl: String)
}

suspend fun sendDocuments(documents: List<Document>): ResultData<Unit> {
val response = client.post("$baseUrl/api/document") {
val effectiveBaseUrl = getEffectiveBaseUrl()
val response = client.post("$effectiveBaseUrl/api/document") {
contentType(ContentType.Application.Json)
setBody(documents.map { it.toApi() })
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.writeopia.core.folders.api

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.http.isSuccess
import io.writeopia.common.utils.ResultData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

/**
* Manages connections to self-hosted backends
*/
class SelfHostedBackendManager(
private val client: HttpClient
) {
private val _connectionState = MutableStateFlow<SelfHostedConnectionState>(SelfHostedConnectionState.NotConnected)
val connectionState: StateFlow<SelfHostedConnectionState> = _connectionState.asStateFlow()

/**
* Tests the connection to a self-hosted backend
*/
suspend fun testConnection(url: String): ResultData<Boolean> {
return try {
_connectionState.value = SelfHostedConnectionState.Connecting

val response = client.get("$url/api/self-hosted/status")

if (response.status.isSuccess()) {
val statusResponse = response.body<Map<String, String>>()

if (statusResponse["status"] == "running") {
_connectionState.value = SelfHostedConnectionState.Connected(url)
ResultData.Complete(true)
} else {
_connectionState.value = SelfHostedConnectionState.Error("Backend is not running properly")
ResultData.Error("Backend is not running properly")
}
} else {
_connectionState.value = SelfHostedConnectionState.Error("Could not connect to backend")
ResultData.Error("Could not connect to backend")
}
} catch (e: Exception) {
_connectionState.value = SelfHostedConnectionState.Error(e.message ?: "Unknown error")
ResultData.Error(e.message ?: "Unknown error")
}
}

/**
* Sets the current backend URL for the app
*/
fun setBackendUrl(url: String) {
_connectionState.value = SelfHostedConnectionState.Connected(url)
}

/**
* Disconnects from the current backend
*/
fun disconnect() {
_connectionState.value = SelfHostedConnectionState.NotConnected
}
}

sealed class SelfHostedConnectionState {
data object NotConnected : SelfHostedConnectionState()
data object Connecting : SelfHostedConnectionState()
data class Connected(val url: String) : SelfHostedConnectionState()
data class Error(val message: String) : SelfHostedConnectionState()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package io.writeopia.core.folders.di

import io.ktor.client.HttpClient
import io.writeopia.core.configuration.repository.ConfigurationRepository
import io.writeopia.core.folders.api.DocumentsApi
import io.writeopia.core.folders.api.SelfHostedBackendManager
import io.writeopia.core.folders.sync.DocumentConflictHandler
import io.writeopia.core.folders.sync.DocumentsSync
import io.writeopia.sdk.repository.DocumentRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch

/**
* Class for injecting documents-related dependencies
*/
class DocumentsInjection(
private val httpClient: HttpClient,
private val documentRepository: DocumentRepository,
private val configurationRepository: ConfigurationRepository,
private val baseUrl: String = "https://writeopia.io"
) {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

private var selfHostedBackendManager: SelfHostedBackendManager? = null
private var documentsApi: DocumentsApi? = null
private var documentsSync: DocumentsSync? = null

private val _isSyncEnabled = MutableStateFlow(false)
val isSyncEnabled = _isSyncEnabled.asStateFlow()

init {
coroutineScope.launch {
val userId = "disconnected_user" // This should be replaced with actual user ID
val selfHostedUrl = configurationRepository.loadSelfHostedBackendUrl(userId)

if (selfHostedUrl != null) {
provideSelfHostedBackendManager().setBackendUrl(selfHostedUrl)
_isSyncEnabled.value = true
}
}
}

fun provideSelfHostedBackendManager(): SelfHostedBackendManager {
return selfHostedBackendManager ?: SelfHostedBackendManager(httpClient).also {
selfHostedBackendManager = it
}
}

fun provideDocumentsApi(): DocumentsApi {
return documentsApi ?: DocumentsApi(
client = httpClient,
baseUrl = baseUrl,
selfHostedBackendManager = provideSelfHostedBackendManager()
).also {
documentsApi = it
}
}

fun provideDocumentsSync(): DocumentsSync {
return documentsSync ?: DocumentsSync(
documentRepository = documentRepository,
documentsApi = provideDocumentsApi(),
documentConflictHandler = DocumentConflictHandler(documentRepository),
selfHostedBackendManager = provideSelfHostedBackendManager()
).also {
documentsSync = it
}
}

/**
* Connects to a self-hosted backend
*/
suspend fun connectToSelfHostedBackend(url: String): Boolean {
val manager = provideSelfHostedBackendManager()
val result = manager.testConnection(url)

if (result is io.writeopia.common.utils.ResultData.Complete && result.data) {
val userId = "disconnected_user" // This should be replaced with actual user ID
configurationRepository.saveSelfHostedBackendUrl(url, userId)
_isSyncEnabled.value = true
return true
}

return false
}

/**
* Disconnects from the current self-hosted backend
*/
suspend fun disconnectFromSelfHostedBackend() {
val userId = "disconnected_user" // This should be replaced with actual user ID
configurationRepository.clearSelfHostedBackendUrl(userId)
provideSelfHostedBackendManager().disconnect()
_isSyncEnabled.value = false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ package io.writeopia.core.folders.sync

import io.writeopia.common.utils.ResultData
import io.writeopia.core.folders.api.DocumentsApi
import io.writeopia.core.folders.api.SelfHostedBackendManager
import io.writeopia.sdk.repository.DocumentRepository
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

class DocumentsSync(
private val documentRepository: DocumentRepository,
private val documentsApi: DocumentsApi,
private val documentConflictHandler: DocumentConflictHandler
private val documentConflictHandler: DocumentConflictHandler,
private val selfHostedBackendManager: SelfHostedBackendManager? = null
) {

/**
Expand Down
Loading