diff --git a/app/build.gradle b/app/build.gradle index e39e7fd..1e1628d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -100,6 +100,13 @@ dependencies { implementation "androidx.hilt:hilt-navigation-compose:1.0.0" kapt "com.google.dagger:hilt-compiler:${hilt_version}" + // Coil (image loading library) + implementation "io.coil-kt:coil:2.4.0" + implementation "io.coil-kt:coil-compose:2.4.0" + + // Disk LRU Cache + implementation "com.jakewharton:disklrucache:2.0.2" + debugImplementation 'androidx.compose.ui:ui-tooling:1.4.3' } diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt index f5800b6..b605c0b 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt @@ -1,35 +1,28 @@ package net.buzzert.kordophonedroid.ui.messagelist import android.util.Log -import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material.Button -import androidx.compose.material.Icon -import androidx.compose.material.IconButton +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text -import androidx.compose.material.TopAppBar -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -40,38 +33,53 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.Coil +import coil.ImageLoader +import coil.ImageLoaderFactory +import coil.compose.AsyncImage +import coil.compose.SubcomposeAsyncImage +import coil.imageLoader +import coil.request.ImageRequest +import coil.size.Size import net.buzzert.kordophone.backend.model.GUID -import net.buzzert.kordophone.backend.model.Message +import net.buzzert.kordophonedroid.R import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import java.net.URL import java.text.SimpleDateFormat import java.time.Duration -import java.time.Instant -import java.time.LocalDate -import java.time.Period -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle import java.util.Date private val IncomingChatBubbleShape = RoundedCornerShape(4.dp, 20.dp, 20.dp, 20.dp) private val OutgoingChatBubbleShape = RoundedCornerShape(20.dp, 4.dp, 20.dp, 20.dp) -data class MessageListViewItem( - val text: String, +data class MessageMetadata( val fromAddress: String, val fromMe: Boolean, val date: Date, val delivered: Boolean = true, ) +interface MessageMetadataProvider { + val metadata: MessageMetadata +} + +sealed class MessageListItem: MessageMetadataProvider { + data class TextMessage(val text: String, override val metadata: MessageMetadata): MessageListItem() + data class ImageAttachmentMessage(val guid: String, override val metadata: MessageMetadata): MessageListItem() +} + @Composable fun MessageListScreen( conversationGUID: GUID, @@ -79,7 +87,8 @@ fun MessageListScreen( viewModel: MessageListViewModel = hiltViewModel(), ) { viewModel.conversationGUID = conversationGUID - + + // Synchronize on launch LaunchedEffect(Unit) { Log.d("MessageListScreen", "Launched effect") viewModel.markAsRead() @@ -88,14 +97,32 @@ fun MessageListScreen( val messages by viewModel.messages.collectAsStateWithLifecycle(initialValue = listOf()) - val messageItems = messages.map { - MessageListViewItem( - text = it.text, - fromMe = it.sender == null, - date = it.date, - fromAddress = it.sender ?: "", - delivered = !viewModel.isPendingMessage(it) + var messageItems = mutableListOf() + for (message in messages) { + val metadata = MessageMetadata( + fromMe = message.sender == null, + date = message.date, + fromAddress = message.sender ?: "", + delivered = !viewModel.isPendingMessage(message) ) + + // Collect attachments + message.attachmentGUIDs?.let { guids -> + guids.forEach { guid -> + val item = MessageListItem.ImageAttachmentMessage( + guid = guid, + metadata = metadata + ) + + messageItems.add(item) + } + } + + val displayText = message.displayText.trim() + if (displayText.isNotEmpty()) { + val textMessage = MessageListItem.TextMessage(text = displayText, metadata = metadata) + messageItems.add(textMessage) + } } Scaffold(topBar = { KordophoneTopAppBar(title = viewModel.title, backAction = backAction) }) { padding -> @@ -112,7 +139,7 @@ fun MessageListScreen( @Composable fun MessageTranscript( - messages: List, + messages: List, paddingValues: PaddingValues, showSenders: Boolean, onSendMessage: (text: String) -> Unit, @@ -149,7 +176,7 @@ fun MessageTranscript( @Composable fun Messages( - messages: List, + messages: List, showSenders: Boolean, modifier: Modifier = Modifier, scrollState: LazyListState @@ -169,14 +196,14 @@ fun Messages( for (index in messages.indices) { val content = messages[index] - var previousMessage: MessageListViewItem? = null + var previousMessage: MessageListItem? = null if ((index + 1) < messages.count()) { previousMessage = messages[index + 1] } val duration: Duration? = if (previousMessage != null) Duration.between( - previousMessage.date.toInstant(), - content.date.toInstant() + previousMessage.metadata.date.toInstant(), + content.metadata.date.toInstant() ) else null val leapMessage = ( @@ -187,23 +214,32 @@ fun Messages( val repeatMessage = !leapMessage && ( previousMessage == null || ( - (previousMessage.fromAddress == content.fromAddress) + (previousMessage.metadata.fromAddress == content.metadata.fromAddress) ) ) // Remember: This is upside down. item { - MessageBubble( - text = content.text, - mine = content.fromMe, - modifier = Modifier - .alpha(if (!content.delivered) 0.5F else 1.0f) - ) + when (content) { + is MessageListItem.TextMessage -> { + MessageBubble( + text = content.text, + mine = content.metadata.fromMe, + modifier = Modifier + .alpha(if (!content.metadata.delivered) 0.5F else 1.0f) + ) + } + + is MessageListItem.ImageAttachmentMessage -> { + ImageBubble(guid = content.guid, mine = content.metadata.fromMe) + } + } + // Sender - if (!content.fromMe && showSenders && !repeatMessage) { + if (!content.metadata.fromMe && showSenders && !repeatMessage) { Text( - text = content.fromAddress, + text = content.metadata.fromAddress, style = MaterialTheme.typography.subtitle2.copy( color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f) ), @@ -214,7 +250,7 @@ fun Messages( // Greater than 30 minutes: show date: if (duration != null) { if (duration.toMinutes() > 30) { - val formattedDate = dateFormatter.format(content.date) + val formattedDate = dateFormatter.format(content.metadata.date) Text( text = formattedDate, textAlign = TextAlign.Center, @@ -239,32 +275,22 @@ fun Messages( } @Composable -fun MessageBubble( - text: String, +fun BubbleScaffold( mine: Boolean, - modifier: Modifier = Modifier, + modifier: Modifier, + content: @Composable () -> Unit ) { - val backgroundBubbleColor = if (mine) MaterialTheme.colors.primary else MaterialTheme.colors.secondary - Column() { Row(modifier = modifier.fillMaxWidth()) { - if (mine) { Spacer(modifier = Modifier.weight(1f)) } + if (mine) { + Spacer(modifier = Modifier.weight(1f)) + } Row( modifier = Modifier.fillMaxWidth(0.8f), horizontalArrangement = if (mine) Arrangement.End else Arrangement.Start, ) { - Surface( - color = backgroundBubbleColor, - shape = if (mine) OutgoingChatBubbleShape else IncomingChatBubbleShape, - ) { - Text( - text = text, - style = MaterialTheme.typography.body2, - modifier = Modifier - .padding(12.dp) - ) - } + content() } if (!mine) { Spacer(modifier = Modifier.weight(1f)) } @@ -274,15 +300,101 @@ fun MessageBubble( } } +@Composable +fun MessageBubble( + text: String, + mine: Boolean, + modifier: Modifier = Modifier, +) { + val backgroundBubbleColor = if (mine) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + + BubbleScaffold(mine = mine, modifier = modifier) { + Surface( + color = backgroundBubbleColor, + shape = if (mine) OutgoingChatBubbleShape else IncomingChatBubbleShape, + ) { + Text( + text = text, + style = MaterialTheme.typography.body2, + modifier = Modifier + .padding(12.dp) + ) + } + } +} + +@Composable +fun ImageBubble( + guid: String, + mine: Boolean, + modifier: Modifier = Modifier, +) { + val shape: RoundedCornerShape = if (mine) OutgoingChatBubbleShape else IncomingChatBubbleShape + val attachmentFetchData = AttachmentFetchData(guid, preview = true) + + BubbleScaffold(mine = mine, modifier = modifier) { + SubcomposeAsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(attachmentFetchData) + .crossfade(true) + .diskCacheKey(attachmentFetchData.guid) + .memoryCacheKey(attachmentFetchData.guid) + .build(), + loading = { + Box( + modifier = Modifier + .background(Color.LightGray) + .size(width = 220.dp, height = 200.dp) + ) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } + }, + error = { + Text("(Error loading attachment)") + }, + contentDescription = "Image attachment", + modifier = Modifier + .clip(shape) + ) + } +} + // - +private fun testMessageMetadata(fromMe: Boolean, delivered: Boolean): MessageMetadata { + return MessageMetadata( + fromMe = fromMe, + fromAddress = if (fromMe) "" else "cool@cool.com", + date = Date(), + delivered = delivered, + ) +} + +private fun makeTestTextMessageItem(text: String, fromMe: Boolean, delivered: Boolean = true): MessageListItem { + return MessageListItem.TextMessage( + text = text, + metadata = testMessageMetadata(fromMe = fromMe, delivered = delivered) + ) +} + +private fun makeTestImageMessageItem(fromMe: Boolean, delivered: Boolean = true): MessageListItem { + return MessageListItem.ImageAttachmentMessage( + guid = "asdf", + metadata = testMessageMetadata(fromMe, delivered) + ) +} + @Preview @Composable private fun MessageListScreenPreview() { - val messages = listOf( - MessageListViewItem(text = "Hello", fromMe = false, date = Date(), fromAddress = "cool@cool.com"), - MessageListViewItem(text = "Hey there, this is a longer text message that might wrap to another line", fromMe = true, date = Date(), fromAddress = ""), - MessageListViewItem(text = "How's it going", fromMe = true, delivered = false, date = Date(), fromAddress = "") + val messages = listOf( + makeTestImageMessageItem(false), + + makeTestTextMessageItem("Hello", false), + makeTestTextMessageItem( "Hey there, this is a longer text message that might wrap to another line", true), + makeTestTextMessageItem("How's it going", fromMe = true, delivered = false) ).reversed() Scaffold() { diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt index 9ee845f..a2569ca 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt @@ -1,11 +1,27 @@ package net.buzzert.kordophonedroid.ui.messagelist +import android.content.Context import android.util.Log import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import coil.Coil +import coil.ComponentRegistry +import coil.ImageLoader +import coil.ImageLoaderFactory +import coil.decode.DataSource +import coil.decode.ImageSource +import coil.disk.DiskCache +import coil.fetch.FetchResult +import coil.fetch.Fetcher +import coil.fetch.SourceResult +import coil.memory.MemoryCache +import coil.request.CachePolicy +import coil.request.Options import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asFlow @@ -19,14 +35,29 @@ import net.buzzert.kordophone.backend.model.Conversation import net.buzzert.kordophone.backend.model.GUID import net.buzzert.kordophone.backend.model.Message import net.buzzert.kordophone.backend.server.ChatRepository +import okio.ByteString.Companion.toByteString +import java.io.BufferedInputStream +import java.io.ByteArrayInputStream +import java.nio.file.FileSystem import java.util.Date import java.util.UUID +import java.util.concurrent.TimeUnit import javax.inject.Inject +import javax.xml.transform.Source +import kotlin.time.Duration.Companion.seconds + +const val MVM_LOG: String = "MessageListViewModel" + +data class AttachmentFetchData( + val guid: String, + val preview: Boolean = false +) @HiltViewModel class MessageListViewModel @Inject constructor( private val repository: ChatRepository, -) : ViewModel() + @ApplicationContext val application: Context, +) : ViewModel(), ImageLoaderFactory, Fetcher.Factory { var conversationGUID: GUID? = null set(value) { @@ -47,6 +78,9 @@ class MessageListViewModel @Inject constructor( init { // TODO: Need to handle settings changes here!! + // Register Coil image loader + Coil.setImageLoader(this) + viewModelScope.launch { // Remove pending message after message is delivered. // By now, the repository should've committed this to the store. @@ -77,6 +111,7 @@ class MessageListViewModel @Inject constructor( sender = null, date = Date(), conversation = conversation!!, + attachmentGUIDs = emptyList(), ) val outgoingGUID = repository.enqueueOutgoingMessage(outgoingMessage, conversation!!) @@ -94,4 +129,49 @@ class MessageListViewModel @Inject constructor( fun synchronize() = viewModelScope.launch { repository.synchronizeConversation(conversation!!, limit = 100) } + + override fun newImageLoader(): ImageLoader { + val factory = this + return ImageLoader.Builder(application) + .memoryCache { + MemoryCache.Builder(application) + .maxSizePercent(0.25) + .build() + } + .diskCache { + DiskCache.Builder() + .directory(application.cacheDir.resolve("attachments")) + .maxSizePercent(0.02) + .build() + } + .components { + // Adds the FetcherFactory + add(factory) + } + .build() + } + + override fun create( + data: AttachmentFetchData, + options: Options, + imageLoader: ImageLoader + ): Fetcher { + return AttachmentFetcher(repository, application, data) + } +} + +private class AttachmentFetcher( + val repository: ChatRepository, + val context: Context, + val data: AttachmentFetchData +): Fetcher { + override suspend fun fetch(): FetchResult { + Log.d(MVM_LOG, "Loading attachment ${data.guid} from network") + val source = repository.fetchAttachmentDataSource(data.guid, data.preview) + return SourceResult( + source = ImageSource(source, context), + dataSource = DataSource.NETWORK, + mimeType = null, + ) + } } \ No newline at end of file diff --git a/backend/chat-cache-test b/backend/chat-cache-test deleted file mode 100644 index 473f399..0000000 Binary files a/backend/chat-cache-test and /dev/null differ diff --git a/backend/chat-cache-test.lock b/backend/chat-cache-test.lock deleted file mode 100644 index 24d6e22..0000000 Binary files a/backend/chat-cache-test.lock and /dev/null differ diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Message.kt b/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Message.kt index 132be9b..c9c8af1 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Message.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Message.kt @@ -2,8 +2,11 @@ package net.buzzert.kordophone.backend.db.model import android.view.Display.Mode import io.realm.kotlin.Realm +import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.ext.toRealmList import io.realm.kotlin.types.EmbeddedRealmObject import io.realm.kotlin.types.RealmInstant +import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.PrimaryKey import net.buzzert.kordophone.backend.db.model.Conversation @@ -20,6 +23,7 @@ open class Message( var text: String, var sender: String?, var date: RealmInstant, + var attachmentGUIDs: RealmList, var conversationGUID: GUID, ): RealmObject @@ -29,6 +33,7 @@ open class Message( text = "", sender = null, date = RealmInstant.now(), + attachmentGUIDs = realmListOf(), conversationGUID = ObjectId().toString(), ) @@ -38,6 +43,7 @@ open class Message( guid = guid, sender = sender, date = Date.from(date.toInstant()), + attachmentGUIDs = attachmentGUIDs.toList(), conversation = parentConversation, ) } @@ -51,5 +57,8 @@ fun ModelMessage.toDatabaseMessage(outgoing: Boolean = false): Message { sender = from.sender date = from.date.toInstant().toRealmInstant() conversationGUID = from.conversation.guid + from.attachmentGUIDs?.let { + attachmentGUIDs = it.toRealmList() + } } } diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/model/Message.kt b/backend/src/main/java/net/buzzert/kordophone/backend/model/Message.kt index 1318cba..45c29ad 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/model/Message.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/model/Message.kt @@ -17,6 +17,9 @@ data class Message( @SerializedName("date") val date: Date, + @SerializedName("fileTransferGUIDs") + val attachmentGUIDs: List?, + @Transient var conversation: Conversation, ) { @@ -27,11 +30,18 @@ data class Message( text = text, sender = sender, date = Date(), + attachmentGUIDs = emptyList(), conversation = conversation, ) } } + val displayText: String get() { + // Filter out attachment markers + val attachmentMarker = byteArrayOf(0xEF.toByte(), 0xBF.toByte(), 0xBC.toByte()).decodeToString() + return text.replace(attachmentMarker, "") + } + override fun toString(): String { return "Message(guid=$guid, text=$text, sender=$sender, date=$date, parent=${conversation.guid})" } diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/server/APIInterface.kt b/backend/src/main/java/net/buzzert/kordophone/backend/server/APIInterface.kt index cbe9b3b..e3014f9 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/server/APIInterface.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/server/APIInterface.kt @@ -69,6 +69,9 @@ interface APIInterface { @POST("/markConversation") suspend fun markConversation(@Query("guid") conversationGUID: String): Response + @GET("/attachment") + suspend fun fetchAttachment(@Query("guid") guid: String, @Query("preview") preview: Boolean = false): ResponseBody + @POST("/authenticate") suspend fun authenticate(@Body request: AuthenticationRequest): Response } diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt b/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt index 182e818..c47a271 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt @@ -21,6 +21,8 @@ import net.buzzert.kordophone.backend.model.Conversation import net.buzzert.kordophone.backend.model.GUID import net.buzzert.kordophone.backend.model.Message import net.buzzert.kordophone.backend.model.UpdateItem +import okhttp3.OkHttpClient +import okio.BufferedSource import java.lang.Error import java.net.URL import java.util.Queue @@ -189,6 +191,10 @@ class ChatRepository( Log.e(REPO_LOG, "Error marking conversation as read: ${e.message}") } + suspend fun fetchAttachmentDataSource(guid: String, preview: Boolean): BufferedSource { + return apiInterface.fetchAttachment(guid, preview).source() + } + fun close() { database.close() } @@ -262,7 +268,8 @@ class ChatRepository( text = outgoingMessage.text, sender = null, conversation = it.conversation, - date = outgoingMessage.date + date = outgoingMessage.date, + attachmentGUIDs = null, ) _messageDeliveredChannel.emit( diff --git a/backend/src/test/java/net/buzzert/kordophone/backend/BackendTests.kt b/backend/src/test/java/net/buzzert/kordophone/backend/BackendTests.kt index 0cdc907..3d294c6 100644 --- a/backend/src/test/java/net/buzzert/kordophone/backend/BackendTests.kt +++ b/backend/src/test/java/net/buzzert/kordophone/backend/BackendTests.kt @@ -11,6 +11,7 @@ import net.buzzert.kordophone.backend.db.CachedChatDatabase import net.buzzert.kordophone.backend.model.Message import net.buzzert.kordophone.backend.server.APIClient import net.buzzert.kordophone.backend.server.APIInterface +import net.buzzert.kordophone.backend.server.Authentication import net.buzzert.kordophone.backend.server.ChatRepository import net.buzzert.kordophone.backend.server.RetrofitAPIClient import net.buzzert.kordophone.backend.server.UpdateMonitor @@ -23,7 +24,7 @@ import java.util.concurrent.TimeUnit class BackendTests { private fun liveRepository(host: String): Pair { - val client = RetrofitAPIClient(URL(host)) + val client = RetrofitAPIClient(URL(host), authentication = Authentication("test", "test")) val database = CachedChatDatabase.testDatabase() val repository = ChatRepository(client, database) return Pair(repository, client) diff --git a/backend/src/test/java/net/buzzert/kordophone/backend/MockServer.kt b/backend/src/test/java/net/buzzert/kordophone/backend/MockServer.kt index 72c9d64..574cb89 100644 --- a/backend/src/test/java/net/buzzert/kordophone/backend/MockServer.kt +++ b/backend/src/test/java/net/buzzert/kordophone/backend/MockServer.kt @@ -29,6 +29,7 @@ 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 @@ -53,6 +54,7 @@ class MockServer { guid = UUID.randomUUID().toString(), sender = null, conversation = parentConversation, + attachmentGUIDs = null, ) } @@ -150,7 +152,8 @@ class MockServer { date = Date(), guid = UUID.randomUUID().toString(), sender = null, // me - conversation = conversation + conversation = conversation, + attachmentGUIDs = null, ) addMessagesToConversation(conversation, listOf(message)) @@ -169,14 +172,15 @@ class MockServerClient(private val server: MockServer): APIClient, WebSocketList override fun getWebSocketClient( serverPath: String, - authToken: String?, + queryParams: Map?, listener: WebSocketListener ): WebSocket { val webServer = server.getServer() + val params = queryParams ?: mapOf() val baseHTTPURL: HttpUrl = webServer.url("/") val baseURL = baseHTTPURL.toUrl() - val requestURL = baseURL.authenticatedWebSocketURL(serverPath, authToken) + val requestURL = baseURL.authenticatedWebSocketURL(serverPath, params) val request = Request.Builder() .url(requestURL) .build() @@ -260,6 +264,10 @@ class MockServerInterface(private val server: MockServer): APIInterface { return Response.success(null) } + override suspend fun fetchAttachment(guid: String, preview: Boolean): ResponseBody { + TODO("Not yet implemented") + } + override suspend fun authenticate(request: AuthenticationRequest): Response { // Anything goes! val response = AuthenticationResponse(