Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2afecafcf | |||
| 50e9971694 |
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
@@ -1,170 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
android:fillColor="@color/black"
|
||||
>
|
||||
<gradient
|
||||
android:startColor="#000"
|
||||
android:endColor="#333"
|
||||
android:angle="1.0"
|
||||
/>
|
||||
</shape>
|
||||
|
||||
BIN
app/src/main/res/drawable/kordophone_ic.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
app/src/main/res/drawable/kordophone_ic_small.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 840 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.0 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 6.8 KiB |
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#3D3D3D</color>
|
||||
</resources>
|
||||
@@ -97,6 +97,7 @@ class CachedChatDatabase (private val realmConfig: RealmConfiguration) {
|
||||
date = conversation.date
|
||||
unreadCount = conversation.unreadCount
|
||||
lastMessagePreview = conversation.lastMessagePreview
|
||||
lastMessageGUID = conversation.lastMessageGUID
|
||||
}
|
||||
} catch (e: NoSuchElementException) {
|
||||
// Conversation does not exist. Copy it to the realm.
|
||||
@@ -121,6 +122,10 @@ class CachedChatDatabase (private val realmConfig: RealmConfiguration) {
|
||||
}
|
||||
|
||||
fun writeMessages(messages: List<ModelMessage>, conversation: ModelConversation, outgoing: Boolean = false) {
|
||||
if (messages.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val dbConversation = getManagedConversationByGuid(conversation.guid)
|
||||
realm.writeBlocking {
|
||||
messages
|
||||
@@ -128,8 +133,17 @@ class CachedChatDatabase (private val realmConfig: RealmConfiguration) {
|
||||
.map { copyToRealm(it, updatePolicy = UpdatePolicy.ALL) }
|
||||
|
||||
findLatest(dbConversation)?.let {
|
||||
it.lastMessagePreview = messages.last().displayText
|
||||
it.date = messages.last().date.toInstant().toRealmInstant()
|
||||
val lastMessage = messages.maxByOrNull { it.date }!!
|
||||
|
||||
val lastMessageDate = lastMessage.date.toInstant().toRealmInstant()
|
||||
if (lastMessageDate > it.date) {
|
||||
it.lastMessageGUID = lastMessage.guid
|
||||
it.lastMessagePreview = lastMessage.displayText
|
||||
|
||||
// This will cause sort order to change. I think this ends
|
||||
// up getting updated whenever we get conversation changes anyway.
|
||||
// it.date = lastMessageDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
package net.buzzert.kordophone.backend.db.model
|
||||
|
||||
import io.realm.kotlin.Realm
|
||||
import io.realm.kotlin.ext.realmListOf
|
||||
import io.realm.kotlin.ext.realmSetOf
|
||||
import io.realm.kotlin.ext.toRealmList
|
||||
import io.realm.kotlin.types.RealmInstant
|
||||
import io.realm.kotlin.types.RealmList
|
||||
import io.realm.kotlin.types.RealmObject
|
||||
import io.realm.kotlin.types.RealmSet
|
||||
import io.realm.kotlin.types.annotations.PrimaryKey
|
||||
import net.buzzert.kordophone.backend.model.GUID
|
||||
import org.mongodb.kbson.ObjectId
|
||||
import java.time.Instant
|
||||
import net.buzzert.kordophone.backend.model.Conversation as ModelConversation
|
||||
import java.util.Date
|
||||
|
||||
@@ -24,6 +20,7 @@ open class Conversation(
|
||||
var date: RealmInstant,
|
||||
var unreadCount: Int,
|
||||
|
||||
var lastMessageGUID: String?,
|
||||
var lastMessagePreview: String?,
|
||||
): RealmObject
|
||||
{
|
||||
@@ -35,10 +32,11 @@ open class Conversation(
|
||||
date = RealmInstant.now(),
|
||||
unreadCount = 0,
|
||||
lastMessagePreview = null,
|
||||
lastMessageGUID = null,
|
||||
)
|
||||
|
||||
fun toConversation(): ModelConversation {
|
||||
val conversation = ModelConversation(
|
||||
return ModelConversation(
|
||||
displayName = displayName,
|
||||
participants = participants.toList(),
|
||||
date = Date.from(date.toInstant()),
|
||||
@@ -46,9 +44,8 @@ open class Conversation(
|
||||
guid = guid,
|
||||
lastMessagePreview = lastMessagePreview,
|
||||
lastMessage = null,
|
||||
lastFetchedMessageGUID = lastMessageGUID
|
||||
)
|
||||
|
||||
return conversation
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -57,6 +54,10 @@ open class Conversation(
|
||||
val o = other as Conversation
|
||||
return guid == o.guid
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return guid.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
fun ModelConversation.toDatabaseConversation(): Conversation {
|
||||
@@ -67,6 +68,7 @@ fun ModelConversation.toDatabaseConversation(): Conversation {
|
||||
date = from.date.toInstant().toRealmInstant()
|
||||
unreadCount = from.unreadCount
|
||||
lastMessagePreview = from.lastMessagePreview
|
||||
lastMessageGUID = from.lastFetchedMessageGUID
|
||||
guid = from.guid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ data class Conversation(
|
||||
|
||||
@SerializedName("lastMessage")
|
||||
var lastMessage: Message?,
|
||||
|
||||
var lastFetchedMessageGUID: String?,
|
||||
) {
|
||||
companion object {
|
||||
fun generate(): Conversation {
|
||||
@@ -38,6 +40,7 @@ data class Conversation(
|
||||
unreadCount = 0,
|
||||
lastMessagePreview = null,
|
||||
lastMessage = null,
|
||||
lastFetchedMessageGUID = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -59,7 +62,7 @@ data class Conversation(
|
||||
participants == o.participants &&
|
||||
displayName == o.displayName &&
|
||||
unreadCount == o.unreadCount &&
|
||||
lastMessagePreview == o.lastMessagePreview
|
||||
lastFetchedMessageGUID == o.lastFetchedMessageGUID
|
||||
)
|
||||
}
|
||||
|
||||
@@ -69,8 +72,8 @@ data class Conversation(
|
||||
result = 31 * result + participants.hashCode()
|
||||
result = 31 * result + (displayName?.hashCode() ?: 0)
|
||||
result = 31 * result + unreadCount
|
||||
result = 31 * result + (lastMessagePreview?.hashCode() ?: 0)
|
||||
result = 31 * result + (lastMessage?.hashCode() ?: 0)
|
||||
result = 31 * result + (lastFetchedMessageGUID?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,9 +182,7 @@ class ChatRepository(
|
||||
}
|
||||
|
||||
suspend fun synchronizeConversation(conversation: Conversation, limit: Int = 15) = withErrorChannelHandling {
|
||||
// TODO: Should only fetch messages after the last GUID we know about.
|
||||
// But keep in mind that outgoing message GUIDs are fake...
|
||||
val messages = fetchMessages(conversation, limit = limit)
|
||||
val messages = fetchMessages(conversation, limit = limit, afterGUID = conversation.lastFetchedMessageGUID)
|
||||
database.writeMessages(messages, conversation)
|
||||
}
|
||||
|
||||
@@ -233,10 +231,10 @@ class ChatRepository(
|
||||
private suspend fun fetchMessages(
|
||||
conversation: Conversation,
|
||||
limit: Int? = null,
|
||||
before: Message? = null,
|
||||
after: Message? = null,
|
||||
beforeGUID: String? = null,
|
||||
afterGUID: String? = null,
|
||||
): List<Message> {
|
||||
return apiInterface.getMessages(conversation.guid, limit, before?.guid, after?.guid)
|
||||
return apiInterface.getMessages(conversation.guid, limit, beforeGUID, afterGUID)
|
||||
.bodyOnSuccessOrThrow()
|
||||
.onEach { it.conversation = conversation }
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.buzzert.kordophone.backend.db.CachedChatDatabase
|
||||
import net.buzzert.kordophone.backend.model.Message
|
||||
import net.buzzert.kordophone.backend.model.OutgoingMessage
|
||||
import net.buzzert.kordophone.backend.server.APIClient
|
||||
import net.buzzert.kordophone.backend.server.APIInterface
|
||||
import net.buzzert.kordophone.backend.server.Authentication
|
||||
@@ -85,13 +86,18 @@ class BackendTests {
|
||||
val (repository, mockServer) = mockRepository()
|
||||
|
||||
val conversation = mockServer.addTestConversations(1).first()
|
||||
val outgoingMessage = MockServer.generateMessage(conversation)
|
||||
val generatedMessage = MockServer.generateMessage(conversation)
|
||||
val outgoingMessage = OutgoingMessage(
|
||||
body = generatedMessage.text,
|
||||
conversation = conversation,
|
||||
attachmentUris = setOf(),
|
||||
attachmentDataSource = { null },
|
||||
)
|
||||
|
||||
val guid = repository.enqueueOutgoingMessage(outgoingMessage, conversation)
|
||||
repository.enqueueOutgoingMessage(outgoingMessage)
|
||||
|
||||
val event = repository.messageDeliveredChannel.first()
|
||||
assertEquals(event.requestGuid, guid)
|
||||
assertEquals(event.message.text, outgoingMessage.text)
|
||||
assertEquals(event.message.text, outgoingMessage.body)
|
||||
|
||||
repository.close()
|
||||
}
|
||||
|
||||
@@ -18,18 +18,19 @@ import net.buzzert.kordophone.backend.server.AuthenticationRequest
|
||||
import net.buzzert.kordophone.backend.server.AuthenticationResponse
|
||||
import net.buzzert.kordophone.backend.server.SendMessageRequest
|
||||
import net.buzzert.kordophone.backend.server.SendMessageResponse
|
||||
import net.buzzert.kordophone.backend.server.UploadAttachmentResponse
|
||||
import net.buzzert.kordophone.backend.server.authenticatedWebSocketURL
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
@@ -66,7 +67,8 @@ class MockServer {
|
||||
unreadCount = 0,
|
||||
lastMessagePreview = null,
|
||||
lastMessage = null,
|
||||
guid = UUID.randomUUID().toString()
|
||||
guid = UUID.randomUUID().toString(),
|
||||
lastFetchedMessageGUID = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -166,6 +168,8 @@ class MockServerClient(private val server: MockServer): APIClient, WebSocketList
|
||||
private var updateWatchJob: Job? = null
|
||||
private val gson: Gson = Gson()
|
||||
|
||||
override val isConfigured: Boolean = true
|
||||
|
||||
override fun getAPIInterface(): APIInterface {
|
||||
return MockServerInterface(server)
|
||||
}
|
||||
@@ -268,6 +272,13 @@ class MockServerInterface(private val server: MockServer): APIInterface {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun uploadAttachment(
|
||||
filename: String,
|
||||
body: RequestBody
|
||||
): Response<UploadAttachmentResponse> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun authenticate(request: AuthenticationRequest): Response<AuthenticationResponse> {
|
||||
// Anything goes!
|
||||
val response = AuthenticationResponse(
|
||||
|
||||