feat(android): support self-host & multi channels (#12095)

This commit is contained in:
Aki Chang
2025-05-06 16:13:20 +08:00
committed by GitHub
parent 88ceeba5b6
commit 24f52ed649
16 changed files with 225 additions and 191 deletions

View File

@@ -53,14 +53,10 @@ android {
} }
flavorDimensions = ['chanel'] flavorDimensions = ['chanel']
productFlavors { productFlavors {
stable { stable
buildConfigField 'String', 'BASE_URL', '"https://app.affine.pro"' beta
resValue 'string', 'host', '"app.affine.pro"' internal
} canary
canary {
buildConfigField 'String', 'BASE_URL', '"https://affine.fail"'
resValue 'string', 'host', '"affine.fail"'
}
} }
compileOptions { compileOptions {

View File

@@ -3,9 +3,6 @@ package app.affine.pro
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import app.affine.pro.service.CookieStore
import app.affine.pro.utils.dataStore
import app.affine.pro.utils.get
import app.affine.pro.utils.logger.AffineDebugTree import app.affine.pro.utils.logger.AffineDebugTree
import app.affine.pro.utils.logger.CrashlyticsTree import app.affine.pro.utils.logger.CrashlyticsTree
import app.affine.pro.utils.logger.FileTree import app.affine.pro.utils.logger.FileTree
@@ -13,11 +10,6 @@ import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.crashlytics.setCustomKeys import com.google.firebase.crashlytics.setCustomKeys
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber import timber.log.Timber
@HiltAndroidApp @HiltAndroidApp
@@ -39,28 +31,6 @@ class AffineApp : Application() {
Firebase.crashlytics.setCustomKeys { Firebase.crashlytics.setCustomKeys {
key("affine_version", CapacitorConfig.getAffineVersion()) key("affine_version", CapacitorConfig.getAffineVersion())
} }
// init cookies from local
MainScope().launch(Dispatchers.IO) {
val sessionCookieStr = applicationContext.dataStore.get(CookieStore.AFFINE_SESSION)
val userIdCookieStr = applicationContext.dataStore.get(CookieStore.AFFINE_USER_ID)
if (sessionCookieStr.isEmpty() || userIdCookieStr.isEmpty()) {
Timber.i("[init] user has not signed in yet.")
return@launch
}
Timber.i("[init] user already signed in.")
try {
val cookies = listOf(
Cookie.parse(BuildConfig.BASE_URL.toHttpUrl(), sessionCookieStr)
?: error("Parse session cookie fail:[ cookie = $sessionCookieStr ]"),
Cookie.parse(BuildConfig.BASE_URL.toHttpUrl(), userIdCookieStr)
?: error("Parse user id cookie fail:[ cookie = $userIdCookieStr ]"),
)
CookieStore.saveCookies(BuildConfig.BASE_URL.toHttpUrl().host, cookies)
FileTree.get()?.checkAndUploadOldLogs()
} catch (e: Exception) {
Timber.w(e, "[init] load persistent cookies fail.")
}
}
} }
override fun onTerminate() { override fun onTerminate() {

View File

@@ -0,0 +1,52 @@
package app.affine.pro
import android.webkit.WebView
import app.affine.pro.service.CookieStore
import app.affine.pro.utils.dataStore
import app.affine.pro.utils.get
import app.affine.pro.utils.getCurrentServerBaseUrl
import app.affine.pro.utils.logger.FileTree
import com.getcapacitor.Bridge
import com.getcapacitor.WebViewListener
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber
object AuthInitializer {
fun initialize(bridge: Bridge) {
bridge.addWebViewListener(object : WebViewListener() {
override fun onPageLoaded(webView: WebView?) {
bridge.removeWebViewListener(this)
MainScope().launch(Dispatchers.IO) {
try {
val server = bridge.getCurrentServerBaseUrl().toHttpUrl()
val sessionCookieStr = AffineApp.context().dataStore
.get(server.host + CookieStore.AFFINE_SESSION)
val userIdCookieStr = AffineApp.context().dataStore
.get(server.host + CookieStore.AFFINE_USER_ID)
if (sessionCookieStr.isEmpty() || userIdCookieStr.isEmpty()) {
Timber.i("[init] user has not signed in yet.")
return@launch
}
Timber.i("[init] user already signed in.")
val cookies = listOf(
Cookie.parse(server, sessionCookieStr)
?: error("Parse session cookie fail:[ cookie = $sessionCookieStr ]"),
Cookie.parse(server, userIdCookieStr)
?: error("Parse user id cookie fail:[ cookie = $userIdCookieStr ]"),
)
CookieStore.saveCookies(server.host, cookies)
FileTree.get()?.checkAndUploadOldLogs(server)
} catch (e: Exception) {
Timber.w(e, "[init] load persistent cookies fail.")
}
}
}
})
}
}

View File

@@ -13,7 +13,9 @@ import app.affine.pro.plugin.AffineThemePlugin
import app.affine.pro.plugin.AuthPlugin import app.affine.pro.plugin.AuthPlugin
import app.affine.pro.plugin.HashCashPlugin import app.affine.pro.plugin.HashCashPlugin
import app.affine.pro.plugin.NbStorePlugin import app.affine.pro.plugin.NbStorePlugin
import app.affine.pro.repo.WebRepo import app.affine.pro.service.GraphQLService
import app.affine.pro.service.SSEService
import app.affine.pro.service.WebService
import app.affine.pro.utils.dp import app.affine.pro.utils.dp
import com.getcapacitor.BridgeActivity import com.getcapacitor.BridgeActivity
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
@@ -27,7 +29,13 @@ class MainActivity : BridgeActivity(), AIButtonPlugin.Callback, AffineThemePlugi
View.OnClickListener { View.OnClickListener {
@Inject @Inject
lateinit var webRepo: WebRepo lateinit var webService: WebService
@Inject
lateinit var sseService: SSEService
@Inject
lateinit var graphQLService: GraphQLService
init { init {
registerPlugins( registerPlugins(
@@ -56,6 +64,11 @@ class MainActivity : BridgeActivity(), AIButtonPlugin.Callback, AffineThemePlugi
} }
} }
override fun load() {
super.load()
AuthInitializer.initialize(bridge)
}
override fun present() { override fun present() {
lifecycleScope.launch { lifecycleScope.launch {
fab.show() fab.show()
@@ -85,7 +98,9 @@ class MainActivity : BridgeActivity(), AIButtonPlugin.Callback, AffineThemePlugi
override fun onClick(v: View) { override fun onClick(v: View) {
lifecycleScope.launch { lifecycleScope.launch {
webRepo.init(bridge) webService.update(bridge)
sseService.updateServer(bridge)
graphQLService.updateServer(bridge)
AIActivity.open(this@MainActivity) AIActivity.open(this@MainActivity)
} }
} }

View File

@@ -2,9 +2,9 @@ package app.affine.pro.ai.chat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.affine.pro.repo.GraphQLRepo import app.affine.pro.service.GraphQLService
import app.affine.pro.repo.SSERepo import app.affine.pro.service.SSEService
import app.affine.pro.repo.WebRepo import app.affine.pro.service.WebService
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -18,9 +18,9 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ChatViewModel @Inject constructor( class ChatViewModel @Inject constructor(
private val webRepo: WebRepo, private val webService: WebService,
private val graphQLRepo: GraphQLRepo, private val graphQLService: GraphQLService,
private val sseRepo: SSERepo, private val sseService: SSEService,
) : ViewModel() { ) : ViewModel() {
private lateinit var sessionId: String private lateinit var sessionId: String
@@ -32,17 +32,17 @@ class ChatViewModel @Inject constructor(
init { init {
viewModelScope.launch { viewModelScope.launch {
sessionId = graphQLRepo.createCopilotSession( sessionId = graphQLService.createCopilotSession(
workspaceId = webRepo.workspaceId(), workspaceId = webService.workspaceId(),
docId = webRepo.docId(), docId = webService.docId(),
).getOrElse { ).getOrElse {
Timber.w(it, "Create session failed") Timber.w(it, "Create session failed")
return@launch return@launch
} }
Timber.i("Create session success:[ sessionId = $sessionId].") Timber.i("Create session success:[ sessionId = $sessionId].")
val historyMessages = graphQLRepo.getCopilotHistories( val historyMessages = graphQLService.getCopilotHistories(
workspaceId = webRepo.workspaceId(), workspaceId = webService.workspaceId(),
docId = webRepo.docId(), docId = webService.docId(),
sessionId = sessionId, sessionId = sessionId,
).getOrDefault(emptyList()).map { ).getOrDefault(emptyList()).map {
ChatMessage.from(it) ChatMessage.from(it)
@@ -55,12 +55,12 @@ class ChatViewModel @Inject constructor(
fun sendMessage(message: String) { fun sendMessage(message: String) {
val sendMessage = suspend { val sendMessage = suspend {
graphQLRepo.createCopilotMessage( graphQLService.createCopilotMessage(
sessionId = sessionId, sessionId = sessionId,
message = message, message = message,
).onSuccess { messageId -> ).onSuccess { messageId ->
Timber.i("send message: $messageId") Timber.i("send message: $messageId")
sseRepo.messageStream(sessionId, messageId) sseService.messageStream(sessionId, messageId)
.onEach { .onEach {
Timber.d("On sse message: ${it.getOrNull()}") Timber.d("On sse message: ${it.getOrNull()}")
} }
@@ -70,9 +70,9 @@ class ChatViewModel @Inject constructor(
} }
viewModelScope.launch { viewModelScope.launch {
if (!this@ChatViewModel::sessionId.isInitialized) { if (!this@ChatViewModel::sessionId.isInitialized) {
graphQLRepo.getCopilotSession( graphQLService.getCopilotSession(
workspaceId = webRepo.workspaceId(), workspaceId = webService.workspaceId(),
docId = webRepo.docId(), docId = webService.docId(),
).onSuccess { id -> ).onSuccess { id ->
sessionId = id sessionId = id
Timber.i("Create session: $id") Timber.i("Create session: $id")

View File

@@ -11,6 +11,7 @@ import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin import com.getcapacitor.annotation.CapacitorPlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@@ -144,7 +145,7 @@ class AuthPlugin : Plugin() {
call.reject(response.body.string()) call.reject(response.body.string())
return@launch return@launch
} }
CookieStore.getCookie(endpoint, CookieStore.AFFINE_SESSION)?.let { CookieStore.getCookie(endpoint.toHttpUrl(), CookieStore.AFFINE_SESSION)?.let {
Timber.i("$method sign in success.") Timber.i("$method sign in success.")
Timber.d("Update session [$it]") Timber.d("Update session [$it]")
call.resolve(JSObject().put("token", it)) call.resolve(JSObject().put("token", it))

View File

@@ -1,35 +0,0 @@
package app.affine.pro.repo
import com.getcapacitor.Bridge
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@Singleton
class WebRepo @Inject constructor() {
suspend fun init(bridge: Bridge) {
_workspaceId = eval(bridge, "window.getCurrentWorkspaceId()")
_docId = eval(bridge, "window.getCurrentDocId()")
_docContentInMD = eval(bridge, "window.getCurrentDocContentInMarkdown()")
}
private suspend fun eval(bridge: Bridge, js: String): String {
return suspendCoroutine { continuation ->
bridge.eval(js) { result ->
continuation.resume(result)
}
}
}
private lateinit var _workspaceId: String
private lateinit var _docId: String
private lateinit var _docContentInMD: String
fun workspaceId() = _workspaceId
fun docId() = _docId
fun docContentInMD() = _docContentInMD
}

View File

@@ -1,43 +0,0 @@
package app.affine.pro.service
import app.affine.pro.BuildConfig
import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.api.Mutation
import com.apollographql.apollo.api.Query
import com.apollographql.apollo.api.Subscription
import com.apollographql.apollo.network.okHttpClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GraphQLClient @Inject constructor() {
private val _client: ApolloClient by lazy {
ApolloClient.Builder().serverUrl("${BuildConfig.BASE_URL}/graphql")
.okHttpClient(OkHttp.client)
.build()
}
suspend fun <D : Query.Data> query(query: Query<D>) = withContext(Dispatchers.IO) {
runCatching {
withContext(Dispatchers.IO) {
_client.query(query).execute().dataOrThrow()
}
}
}
suspend fun <D : Mutation.Data> mutation(mutation: Mutation<D>) = withContext(Dispatchers.IO) {
runCatching {
_client.mutation(mutation).execute().dataOrThrow()
}
}
suspend fun <D : Subscription.Data> subscription(subscription: Subscription<D>) =
withContext(Dispatchers.IO) {
runCatching {
_client.subscription(subscription).execute().dataOrThrow()
}
}
}

View File

@@ -1,23 +1,29 @@
package app.affine.pro.repo package app.affine.pro.service
import app.affine.pro.Prompt import app.affine.pro.Prompt
import app.affine.pro.service.GraphQLClient import app.affine.pro.utils.getCurrentServerBaseUrl
import com.affine.pro.graphql.CreateCopilotMessageMutation import com.affine.pro.graphql.CreateCopilotMessageMutation
import com.affine.pro.graphql.CreateCopilotSessionMutation import com.affine.pro.graphql.CreateCopilotSessionMutation
import com.affine.pro.graphql.GetCopilotHistoriesQuery import com.affine.pro.graphql.GetCopilotHistoriesQuery
import com.affine.pro.graphql.GetCopilotSessionsQuery import com.affine.pro.graphql.GetCopilotSessionsQuery
import com.affine.pro.graphql.type.CreateChatMessageInput import com.affine.pro.graphql.type.CreateChatMessageInput
import com.affine.pro.graphql.type.CreateChatSessionInput import com.affine.pro.graphql.type.CreateChatSessionInput
import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.api.Mutation
import com.apollographql.apollo.api.Optional import com.apollographql.apollo.api.Optional
import com.apollographql.apollo.api.Query
import com.apollographql.apollo.api.Subscription
import com.apollographql.apollo.network.okHttpClient
import com.getcapacitor.Bridge
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class GraphQLRepo @Inject constructor( class GraphQLService @Inject constructor() {
private val client: GraphQLClient
) {
suspend fun getCopilotSession(workspaceId: String, docId: String) = client.query( suspend fun getCopilotSession(workspaceId: String, docId: String) = query(
GetCopilotSessionsQuery( GetCopilotSessionsQuery(
workspaceId = workspaceId, workspaceId = workspaceId,
docId = Optional.present(docId) docId = Optional.present(docId)
@@ -30,7 +36,7 @@ class GraphQLRepo @Inject constructor(
workspaceId: String, workspaceId: String,
docId: String, docId: String,
prompt: Prompt = Prompt.ChatWithAFFiNEAI prompt: Prompt = Prompt.ChatWithAFFiNEAI
) = client.mutation( ) = mutation(
CreateCopilotSessionMutation( CreateCopilotSessionMutation(
CreateChatSessionInput( CreateChatSessionInput(
docId = docId, docId = docId,
@@ -46,7 +52,7 @@ class GraphQLRepo @Inject constructor(
workspaceId: String, workspaceId: String,
docId: String, docId: String,
sessionId: String, sessionId: String,
) = client.query( ) = query(
GetCopilotHistoriesQuery( GetCopilotHistoriesQuery(
workspaceId = workspaceId, workspaceId = workspaceId,
docId = Optional.present(docId), docId = Optional.present(docId),
@@ -60,15 +66,49 @@ class GraphQLRepo @Inject constructor(
suspend fun createCopilotMessage( suspend fun createCopilotMessage(
sessionId: String, sessionId: String,
message: String, message: String,
) = client.mutation(CreateCopilotMessageMutation( ) = mutation(
CreateChatMessageInput( CreateCopilotMessageMutation(
sessionId = sessionId, CreateChatMessageInput(
content = Optional.present(message) sessionId = sessionId,
content = Optional.present(message)
)
) )
)).mapCatching { data -> ).mapCatching { data ->
data.createCopilotMessage data.createCopilotMessage
} }
suspend fun updateServer(bridge: Bridge) {
val server = bridge.getCurrentServerBaseUrl()
if (this::_client.isInitialized && _client.newBuilder().httpServerUrl == server) return
_client = ApolloClient.Builder().serverUrl("$server/graphql")
.okHttpClient(OkHttp.client)
.build()
}
private lateinit var _client: ApolloClient
private suspend fun <D : Query.Data> query(query: Query<D>) = withContext(Dispatchers.IO) {
runCatching {
withContext(Dispatchers.IO) {
_client.query(query).execute().dataOrThrow()
}
}
}
private suspend fun <D : Mutation.Data> mutation(mutation: Mutation<D>) =
withContext(Dispatchers.IO) {
runCatching {
_client.mutation(mutation).execute().dataOrThrow()
}
}
private suspend fun <D : Subscription.Data> subscription(subscription: Subscription<D>) =
withContext(Dispatchers.IO) {
runCatching {
_client.subscription(subscription).execute().dataOrThrow()
}
}
companion object { companion object {
private const val ERROR_NULL_SESSION_ID = "null session id." private const val ERROR_NULL_SESSION_ID = "null session id."
} }

View File

@@ -1,6 +1,5 @@
package app.affine.pro.service package app.affine.pro.service
import androidx.core.net.toUri
import app.affine.pro.AffineApp import app.affine.pro.AffineApp
import app.affine.pro.utils.dataStore import app.affine.pro.utils.dataStore
import app.affine.pro.utils.set import app.affine.pro.utils.set
@@ -53,11 +52,11 @@ object CookieStore {
_cookies[host] = cookies _cookies[host] = cookies
MainScope().launch(Dispatchers.IO) { MainScope().launch(Dispatchers.IO) {
cookies.find { it.name == AFFINE_SESSION }?.let { cookies.find { it.name == AFFINE_SESSION }?.let {
AffineApp.context().dataStore.set(AFFINE_SESSION, it.toString()) AffineApp.context().dataStore.set(host + AFFINE_SESSION, it.toString())
} }
cookies.find { it.name == AFFINE_USER_ID }?.let { cookies.find { it.name == AFFINE_USER_ID }?.let {
Timber.d("Update user id [${it.value}]") Timber.d("Update user id [${it.value}]")
AffineApp.context().dataStore.set(AFFINE_USER_ID, it.toString()) AffineApp.context().dataStore.set(host + AFFINE_USER_ID, it.toString())
Firebase.crashlytics.setUserId(it.value) Firebase.crashlytics.setUserId(it.value)
} }
} }
@@ -65,8 +64,8 @@ object CookieStore {
fun getCookies(host: String) = _cookies[host] ?: emptyList() fun getCookies(host: String) = _cookies[host] ?: emptyList()
fun getCookie(url: String, name: String) = url.toUri().host fun getCookie(url: HttpUrl, name: String) = url.host
?.let { _cookies[it] } .let { _cookies[it] }
?.find { cookie -> cookie.name == name } ?.find { cookie -> cookie.name == name }
?.value ?.value
} }

View File

@@ -1,7 +1,7 @@
package app.affine.pro.repo package app.affine.pro.service
import app.affine.pro.BuildConfig import app.affine.pro.utils.getCurrentServerBaseUrl
import app.affine.pro.service.OkHttp import com.getcapacitor.Bridge
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
@@ -17,10 +17,16 @@ import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class SSERepo @Inject constructor() { class SSEService @Inject constructor() {
private lateinit var _server: String
suspend fun updateServer(bridge: Bridge) {
_server = bridge.getCurrentServerBaseUrl()
}
fun messageStream(sessionId: String, messageId: String) = fun messageStream(sessionId: String, messageId: String) =
"${BuildConfig.BASE_URL}/api/copilot/chat/$sessionId/stream?messageId=$messageId".eventSource() "$_server/api/copilot/chat/$sessionId/stream?messageId=$messageId".eventSource()
data class Event(val id: String?, val type: String?, val data: String) data class Event(val id: String?, val type: String?, val data: String)

View File

@@ -0,0 +1,28 @@
package app.affine.pro.service
import app.affine.pro.utils.getCurrentDocContentInMarkdown
import app.affine.pro.utils.getCurrentDocId
import app.affine.pro.utils.getCurrentWorkspaceId
import com.getcapacitor.Bridge
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class WebService @Inject constructor() {
suspend fun update(bridge: Bridge) {
_workspaceId = bridge.getCurrentWorkspaceId()
_docId = bridge.getCurrentDocId()
_docContentInMD = bridge.getCurrentDocContentInMarkdown()
}
private lateinit var _workspaceId: String
private lateinit var _docId: String
private lateinit var _docContentInMD: String
fun workspaceId() = _workspaceId
fun docId() = _docId
fun docContentInMD() = _docContentInMD
}

View File

@@ -1,18 +0,0 @@
package app.affine.pro.service.interceptor
import android.webkit.CookieManager
import app.affine.pro.BuildConfig
import com.apollographql.apollo.api.http.HttpRequest
import com.apollographql.apollo.network.http.HttpInterceptor
import com.apollographql.apollo.network.http.HttpInterceptorChain
object CookieInterceptor : HttpInterceptor {
override suspend fun intercept(
request: HttpRequest,
chain: HttpInterceptorChain
) = chain.proceed(
request.newBuilder().addHeader(
"Cookie", CookieManager.getInstance().getCookie(BuildConfig.BASE_URL)
).build()
)
}

View File

@@ -20,11 +20,3 @@ suspend fun DataStore<Preferences>.set(key: String, value: String) {
suspend fun DataStore<Preferences>.get(key: String) = data.map { suspend fun DataStore<Preferences>.get(key: String) = data.map {
it[stringPreferencesKey(key)] ?: "" it[stringPreferencesKey(key)] ?: ""
}.first() }.first()
suspend fun DataStore<Preferences>.clear(vararg keys: String) {
edit { prefs ->
keys.forEach { key ->
prefs[stringPreferencesKey(key)] = ""
}
}
}

View File

@@ -0,0 +1,30 @@
package app.affine.pro.utils
import com.getcapacitor.Bridge
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun Bridge.getCurrentServerBaseUrl() = eval("window.getCurrentServerBaseUrl()").strict()
suspend fun Bridge.getCurrentWorkspaceId() = eval("window.getCurrentWorkspaceId()").strict()
suspend fun Bridge.getCurrentDocId() = eval("window.getCurrentDocId()").strict()
suspend fun Bridge.getCurrentDocContentInMarkdown() =
eval("window.getCurrentDocContentInMarkdown()").strict()
private suspend fun Bridge.eval(js: String): String {
return suspendCoroutine { continuation ->
eval(js) { result ->
continuation.resume(result)
}
}
}
private fun String.strict() = let {
if (startsWith("\"") && endsWith("\"")) {
substring(1, lastIndex)
} else {
this
}
}

View File

@@ -12,6 +12,7 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@@ -41,19 +42,19 @@ class FileTree(context: Context) : Timber.Tree() {
} }
} }
suspend fun checkAndUploadOldLogs() { suspend fun checkAndUploadOldLogs(server: HttpUrl) {
val today = dateFormat.format(Date()) val today = dateFormat.format(Date())
logDirectory.listFiles()?.forEach { file -> logDirectory.listFiles()?.forEach { file ->
val fileName = file.name val fileName = file.name
if (fileName.endsWith(".log") && !fileName.startsWith(today)) { if (fileName.endsWith(".log") && !fileName.startsWith(today)) {
uploadLogToFirebase(file) uploadLogToFirebase(server, file)
} }
} }
} }
private suspend fun uploadLogToFirebase(file: File) = private suspend fun uploadLogToFirebase(server: HttpUrl, file: File) =
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
val user = CookieStore.getCookie(BuildConfig.BASE_URL, CookieStore.AFFINE_USER_ID) val user = CookieStore.getCookie(server, CookieStore.AFFINE_USER_ID)
?: return@suspendCancellableCoroutine ?: return@suspendCancellableCoroutine
val storageRef = Firebase.storage.reference val storageRef = Firebase.storage.reference
val logFileRef = storageRef.child("android_log/$user/${file.name}") val logFileRef = storageRef.child("android_log/$user/${file.name}")