diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/QuasseldroidRouter.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/QuasseldroidRouter.kt index 8ed54ebf19f9547fc96105e95099c4c78f9bf00c..eff0f83625d731aad420545ac82202d642f8376a 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/QuasseldroidRouter.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/QuasseldroidRouter.kt @@ -7,10 +7,12 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +import de.justjanne.libquassel.protocol.models.ids.BufferId import de.justjanne.quasseldroid.service.QuasselBackend import de.justjanne.quasseldroid.ui.routes.CoreInfoRoute import de.justjanne.quasseldroid.ui.routes.HomeRoute import de.justjanne.quasseldroid.ui.routes.LoginRoute +import de.justjanne.quasseldroid.ui.routes.MessageRoute @Composable fun QuasseldroidRouter(backend: QuasselBackend) { @@ -28,7 +30,7 @@ fun QuasseldroidRouter(backend: QuasselBackend) { "buffer/{bufferId}", listOf(navArgument("bufferId") { type = NavType.IntType }) ) { - Text("Buffer ${it.arguments?.getInt("bufferId")}") + MessageRoute(backend, navController, BufferId(it.arguments?.getInt("bufferId") ?: -1)) } composable("bufferViewConfigs") { Text("List of BufferViewConfigs") diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt index d72031943389e9531229a36a5101ccc475803f39..fe3faa8ba12ba320cd6fc525ba7d02a549e6255a 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt @@ -94,6 +94,14 @@ class MessageStore( } } + fun clear(bufferId: BufferId) { + scope.launch { + state.update { messages -> + messages - bufferId + } + } + } + override fun close() { runBlocking { disposable.cancelAndJoin() diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt index 5965a4375b0d15778e7ef7364ff0bc4835443437..bff8b7e404e87f6bb7aea51b25087441f6004470 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt @@ -10,8 +10,13 @@ import de.justjanne.libquassel.protocol.models.flags.MessageType import de.justjanne.libquassel.protocol.models.ids.BufferId import de.justjanne.libquassel.protocol.models.ids.MsgId import de.justjanne.libquassel.protocol.models.ids.NetworkId +import de.justjanne.libquassel.protocol.util.irc.HostmaskHelper import org.threeten.bp.Instant +class SampleNickProvider : PreviewParameterProvider<String> { + override val values = SampleMessageProvider().values.map { HostmaskHelper.nick(it.sender) } +} + class SampleMessageProvider : PreviewParameterProvider<Message> { override val values = sequenceOf( Message( diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBase.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBase.kt new file mode 100644 index 0000000000000000000000000000000000000000..762d969a16955ea99549f9ddc86734cc1e10a8b3 --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBase.kt @@ -0,0 +1,171 @@ +package de.justjanne.quasseldroid.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.paddingFrom +import androidx.compose.foundation.layout.paddingFromBaseline +import androidx.compose.foundation.layout.width +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.FirstBaseline +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import de.justjanne.libquassel.protocol.models.Message +import de.justjanne.libquassel.protocol.util.irc.HostmaskHelper +import de.justjanne.quasseldroid.sample.SampleMessageProvider +import de.justjanne.quasseldroid.ui.icons.AvatarIcon +import de.justjanne.quasseldroid.ui.theme.QuasselTheme +import de.justjanne.quasseldroid.ui.theme.Typography +import irc.SenderColorUtil +import org.threeten.bp.ZoneId +import org.threeten.bp.format.DateTimeFormatter +import org.threeten.bp.format.FormatStyle + +private val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) + +@Composable +fun buildNick(nick: String, senderPrefixes: String): AnnotatedString { + val senderColor = QuasselTheme.sender.colors[SenderColorUtil.senderColor(nick)] + + return buildAnnotatedString { + if (senderPrefixes.isNotEmpty()) { + append(senderPrefixes) + } + pushStyle(SpanStyle(color = senderColor, fontWeight = FontWeight.Bold)) + append(nick) + pop() + } +} + +@Preview(name = "Message Base", showBackground = true) +@Composable +fun MessageBase( + @PreviewParameter(SampleMessageProvider::class) + message: Message, + followUp: Boolean = false, + // avatarSize: Dp = 32.dp + content: @Composable () -> Unit = { Text(message.content, style = Typography.body2) } +) { + val avatarSize = 32.dp + + val nick = HostmaskHelper.nick(message.sender) + + Row( + modifier = Modifier + .padding(2.dp) + .fillMaxWidth() + ) { + if (!followUp) { + Spacer(Modifier.width(4.dp)) + AvatarIcon( + nick, + size = avatarSize, + modifier = Modifier + .paddingFromBaseline(top = 28.sp) + ) + Spacer(Modifier.width(4.dp)) + } else { + Spacer(Modifier.width(avatarSize + 8.dp)) + } + Column(modifier = Modifier.align(Alignment.CenterVertically)) { + if (!followUp) { + Row { + Text( + buildAnnotatedString { + append(buildNick(nick, message.senderPrefixes)) + append(' ') + pushStyle(SpanStyle(color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium))) + append(message.realName) + pop() + }, + style = Typography.body2 + ) + } + } + Row { + Box(modifier = Modifier.weight(1.0f)) { + content() + } + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Text( + message.time + .atZone(ZoneId.systemDefault()) + .format(formatter), + style = Typography.body2, + fontSize = 12.sp, + modifier = Modifier.align(Alignment.Bottom) + ) + } + } + } + } +} + +@Preview(name = "Message Small", showBackground = true) +@Composable +fun MessageBaseSmall( + @PreviewParameter(SampleMessageProvider::class) + message: Message, + followUp: Boolean = false, + // avatarSize: Dp = 32.dp, + content: @Composable () -> Unit = { + val nick = HostmaskHelper.nick(message.sender) + + Text(buildAnnotatedString { + append("— ") + append(buildNick(nick, message.senderPrefixes)) + append(" ") + append(message.content) + }, style = Typography.body2) + } +) { + val avatarSize = 16.dp + val nick = HostmaskHelper.nick(message.sender) + + Row( + modifier = Modifier + .padding(2.dp) + .fillMaxWidth() + ) { + Spacer(Modifier.width(20.dp)) + AvatarIcon( + nick, + modifier = Modifier + .align(Alignment.Top) + .paddingFromBaseline(top = 14.sp), + size = avatarSize + ) + Spacer(Modifier.width(4.dp)) + Box(modifier = Modifier.weight(1.0f)) { + content() + } + Spacer(Modifier.width(4.dp)) + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Text( + message.time + .atZone(ZoneId.systemDefault()) + .format(formatter), + style = Typography.body2, + fontSize = 12.sp, + modifier = Modifier.align(Alignment.Bottom) + ) + } + } +} diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBaseView.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBaseView.kt deleted file mode 100644 index efe03af838ba32f88249adccfbbf3a8d27f8aaa5..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBaseView.kt +++ /dev/null @@ -1,92 +0,0 @@ -package de.justjanne.quasseldroid.ui.components - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.ContentAlpha -import androidx.compose.material.LocalContentAlpha -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import de.justjanne.libquassel.protocol.models.Message -import de.justjanne.libquassel.protocol.util.irc.HostmaskHelper -import de.justjanne.quasseldroid.ui.icons.AvatarIcon -import de.justjanne.quasseldroid.ui.theme.QuasselTheme -import de.justjanne.quasseldroid.ui.theme.Typography -import irc.SenderColorUtil -import org.threeten.bp.ZoneId -import org.threeten.bp.format.DateTimeFormatter -import org.threeten.bp.format.FormatStyle - -private val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) - -@Composable -fun MessageBaseView( - message: Message, - followUp: Boolean, - avatarSize: Dp, - content: @Composable () -> Unit -) { - val nick = HostmaskHelper.nick(message.sender) - val senderColor = QuasselTheme.sender.colors[SenderColorUtil.senderColor(nick)] - - Row(modifier = Modifier.padding(2.dp)) { - if (!followUp) { - AvatarIcon(nick, null, modifier = Modifier.padding(vertical = 2.dp)) - Spacer(Modifier.width(4.dp)) - } else { - Spacer(Modifier.width(avatarSize + 8.dp)) - } - Column { - if (!followUp) { - Row { - Text( - message.senderPrefixes, - style = Typography.body2, - fontWeight = FontWeight.Bold, - ) - Text( - nick, - style = Typography.body2, - fontWeight = FontWeight.Bold, - color = senderColor, - ) - Spacer(Modifier.width(4.dp)) - CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { - Text( - message.realName, - modifier = Modifier.weight(1.0f), - style = Typography.body2, - overflow = TextOverflow.Ellipsis, - ) - } - } - } - Row { - Box(modifier = Modifier.weight(1.0f)) { - content() - } - CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { - Text( - message.time - .atZone(ZoneId.systemDefault()) - .format(formatter), - style = Typography.body2, - fontSize = 12.sp, - modifier = Modifier.align(Alignment.Bottom) - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageList.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageList.kt index ae018dfd7fd07b832a3714e6be574137bc8a5d0e..3faac0d0cf7795b6d4031348bc833a3f79da403c 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageList.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageList.kt @@ -6,16 +6,26 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp +import de.justjanne.bitflags.of import de.justjanne.libquassel.protocol.models.Message +import de.justjanne.libquassel.protocol.models.flags.MessageType import de.justjanne.libquassel.protocol.models.ids.MsgId +import de.justjanne.libquassel.protocol.util.irc.HostmaskHelper +import de.justjanne.quasseldroid.R import de.justjanne.quasseldroid.sample.SampleMessagesProvider +import de.justjanne.quasseldroid.ui.theme.QuasselTheme import de.justjanne.quasseldroid.ui.theme.Typography import de.justjanne.quasseldroid.util.extensions.OnBottomReached import de.justjanne.quasseldroid.util.extensions.OnTopReached import de.justjanne.quasseldroid.util.extensions.getPrevious +import de.justjanne.quasseldroid.util.format.IrcFormat +import de.justjanne.quasseldroid.util.format.IrcFormatDeserializer +import de.justjanne.quasseldroid.util.format.IrcFormatRenderer +import de.justjanne.quasseldroid.util.format.TextFormatter import org.threeten.bp.ZoneId @Preview(name = "Messages", showBackground = true) @@ -41,21 +51,40 @@ fun MessageList( message.realName == prev.realName && message.avatarUrl == prev.avatarUrl - val isNew = prev != null && - prev.messageId <= markerLine && + val isNew = (prev == null || prev.messageId <= markerLine) && message.messageId > markerLine + val parsed = IrcFormatDeserializer.parse(message.content) + if (prevDate == null || !messageDate.isEqual(prevDate)) { MessageDayChangeView(messageDate, isNew) } else if (isNew) { NewMessageView() } - MessageBaseView(message, followUp, 32.dp) { - Text( - message.content, - style = Typography.body2, - ) + when (message.type) { + MessageType.of(MessageType.Plain) -> { + MessageBase(message, followUp) { + Text(IrcFormatRenderer.render(parsed), style = Typography.body2) + } + } + MessageType.of(MessageType.Action) -> { + MessageBaseSmall(message) { + val nick = HostmaskHelper.nick(message.sender) + + Text( + TextFormatter.format( + AnnotatedString(stringResource(R.string.message_format_action)), + buildNick(nick, message.senderPrefixes), + IrcFormatRenderer.render( + data = parsed.map { it.copy(style = it.style.flipFlag(IrcFormat.Flag.ITALIC)) } + ) + ), + style = Typography.body2, + color = QuasselTheme.chat.onAction + ) + } + } } } } diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/icons/AvatarIcon.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/icons/AvatarIcon.kt index 757b3118b93cedc5c8e2004e9326a8d79a0e5d56..06087cf398b23e8bb6ff2cd81e900bc435a880d7 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/icons/AvatarIcon.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/icons/AvatarIcon.kt @@ -2,6 +2,7 @@ package de.justjanne.quasseldroid.ui.icons import android.graphics.Bitmap import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ContentAlpha @@ -15,23 +16,21 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import de.justjanne.quasseldroid.sample.SampleNickProvider import de.justjanne.quasseldroid.ui.theme.QuasselTheme import irc.SenderColorUtil import java.util.* @Preview -@Composable -private fun AvatarIconPreview() { - AvatarIcon("justJanne", null) -} - @Composable fun AvatarIcon( + @PreviewParameter(SampleNickProvider::class) nick: String, - avatar: Bitmap?, modifier: Modifier = Modifier, + avatar: Bitmap? = null, size: Dp = 32.dp ) { val senderColor = QuasselTheme.sender.colors[SenderColorUtil.senderColor(nick)] diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/HomeRoute.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/HomeRoute.kt index 0b9d1123c47e1cb739b763f025f868e1cbbf5573..940fd34f9f860ee4d57d754855d454b6bfad5e26 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/HomeRoute.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/HomeRoute.kt @@ -1,10 +1,12 @@ package de.justjanne.quasseldroid.ui.routes -import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.material.TextField @@ -14,114 +16,106 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import de.justjanne.libquassel.protocol.models.Message -import de.justjanne.libquassel.protocol.models.ids.BufferId -import de.justjanne.libquassel.protocol.models.ids.MsgId +import de.justjanne.libquassel.protocol.models.BufferInfo +import de.justjanne.libquassel.protocol.models.ids.NetworkId +import de.justjanne.libquassel.protocol.models.network.NetworkInfo +import de.justjanne.libquassel.protocol.syncables.common.Network import de.justjanne.libquassel.protocol.util.flatMap -import de.justjanne.quasseldroid.messages.MessageStore import de.justjanne.quasseldroid.service.QuasselBackend -import de.justjanne.quasseldroid.ui.components.MessageList import de.justjanne.quasseldroid.util.mapNullable import de.justjanne.quasseldroid.util.rememberFlow import de.justjanne.quasseldroid.util.saver.TextFieldValueSaver +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map -private const val limit = 20 - @Composable fun HomeRoute(backend: QuasselBackend, navController: NavController) { - val session = rememberFlow(null) { - backend.flow() - .mapNullable { it.session } + val side = rememberFlow(null) { + backend.flow().mapNullable { it.session.side } } val (buffer, setBuffer) = rememberSaveable(stateSaver = TextFieldValueSaver) { - mutableStateOf(TextFieldValue("3747")) - } - val (position, setPosition) = rememberSaveable(stateSaver = TextFieldValueSaver) { - mutableStateOf(TextFieldValue("108113920")) - } - - val bufferId = BufferId(buffer.text.toIntOrNull() ?: -1) - val positionId = MsgId(position.text.toLongOrNull() ?: -1L) - - val listState = rememberLazyListState() - - val messageStore: MessageStore? = rememberFlow(null) { - backend.flow() - .mapNullable { it.messages } + mutableStateOf(TextFieldValue("")) } - val messages: List<Message> = rememberFlow(emptyList()) { + val initStatus = rememberFlow(null) { backend.flow() - .mapNullable { it.messages } + .mapNullable { it.session } + .mapNullable { it.baseInitHandler } .flatMap() - .mapNullable { it[bufferId] } - .map { it?.messages.orEmpty() } } - val markerLine: MsgId? = rememberFlow(null) { - backend.flow() + val buffers: List<Pair<NetworkInfo?, BufferInfo>> = rememberFlow(emptyList()) { + val sessions = backend.flow() .mapNullable { it.session } .flatMap() + + val networks: Flow<Map<NetworkId, Network>> = sessions + .mapNullable { it.networks } + .map { it.orEmpty() } + + val buffers: Flow<List<BufferInfo>> = sessions .mapNullable { it.bufferSyncer } .flatMap() - .mapNullable { it.markerLines[bufferId] } + .mapNullable { it.bufferInfos.values.sortedBy(BufferInfo::bufferName) } + .map { it.orEmpty() } + + combine(buffers, networks) { bufferList, networkMap -> + bufferList.map { + Pair(networkMap[it.networkId]?.networkInfo(), it) + } + } } - val initStatus = rememberFlow(null) { - backend.flow() - .mapNullable { it.session } - .mapNullable { it.baseInitHandler } - .flatMap() + val filteredBuffers = buffers.filter { (_, info) -> + info.bufferName?.contains(buffer.text) == true } val context = LocalContext.current - val buttonScrollState = rememberScrollState() + + val scrollState = rememberLazyListState() Column { - Text("Side: ${session?.side}") + Text("Side: $side") if (initStatus != null) { val done = initStatus.total - initStatus.waiting.size Text("Init: ${initStatus.started} $done/ ${initStatus.total}") } - Row(modifier = Modifier.horizontalScroll(buttonScrollState)) { - Button(onClick = { navController.navigate("coreInfo") }) { - Text("Core Info") - } - Button(onClick = { - backend.disconnect(context) - navController.navigate("login") - }) { - Text("Disconnect") - } - Button(onClick = { - messageStore?.loadBefore(bufferId, limit) - }) { - Text("↑") - } - Button(onClick = { - messageStore?.loadAfter(bufferId, limit) - }) { - Text("↓") - } - Button(onClick = { - messageStore?.loadAround(bufferId, positionId, limit) - }) { - Text("…") - } + Button(onClick = { navController.navigate("coreInfo") }) { + Text("Core Info") + } + Button(onClick = { + backend.disconnect(context) + navController.navigate("login") + }) { + Text("Disconnect") } TextField(value = buffer, onValueChange = setBuffer) - TextField(value = position, onValueChange = setPosition) - MessageList( - messages = messages, - listState = listState, - markerLine = markerLine ?: MsgId(-1), - buffer = 5, - onLoadAtStart = { messageStore?.loadBefore(bufferId, limit) }, - onLoadAtEnd = { messageStore?.loadAfter(bufferId, limit) } - ) + LazyColumn(state = scrollState) { + items(filteredBuffers, key = { (_, buffer) -> buffer.bufferId }) { (network, buffer) -> + Column(modifier = Modifier + .padding(4.dp) + .fillMaxWidth() + .clickable { navController.navigate("buffer/${buffer.bufferId.id}") } + ) { + Text( + network?.networkName ?: "Unknown network", + modifier = Modifier.fillMaxWidth() + ) + Text( + buffer.type.joinToString(", "), + modifier = Modifier.fillMaxWidth() + ) + Text( + buffer.bufferName ?: "Unknown buffer", + modifier = Modifier.fillMaxWidth() + ) + } + } + } } } diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/MessageRoute.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/MessageRoute.kt new file mode 100644 index 0000000000000000000000000000000000000000..c05908e85dc6ff5195358958085983bfe996044e --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/MessageRoute.kt @@ -0,0 +1,99 @@ +package de.justjanne.quasseldroid.ui.routes + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.TextFieldValue +import androidx.navigation.NavController +import de.justjanne.libquassel.protocol.models.Message +import de.justjanne.libquassel.protocol.models.ids.BufferId +import de.justjanne.libquassel.protocol.models.ids.MsgId +import de.justjanne.libquassel.protocol.util.flatMap +import de.justjanne.quasseldroid.messages.MessageStore +import de.justjanne.quasseldroid.service.QuasselBackend +import de.justjanne.quasseldroid.ui.components.MessageList +import de.justjanne.quasseldroid.util.mapNullable +import de.justjanne.quasseldroid.util.rememberFlow +import de.justjanne.quasseldroid.util.saver.TextFieldValueSaver +import kotlinx.coroutines.flow.map + +private const val limit = 20 + +@Composable +fun MessageRoute( + backend: QuasselBackend, + navController: NavController, + buffer: BufferId +) { + val listState = rememberLazyListState() + + val messageStore: MessageStore? = rememberFlow(null) { + backend.flow() + .mapNullable { it.messages } + } + + val messages: List<Message> = rememberFlow(emptyList()) { + backend.flow() + .mapNullable { it.messages } + .flatMap() + .mapNullable { it[buffer] } + .map { it?.messages.orEmpty() } + } + + val markerLine: MsgId? = rememberFlow(null) { + backend.flow() + .mapNullable { it.session } + .flatMap() + .mapNullable { it.bufferSyncer } + .flatMap() + .mapNullable { it.markerLines[buffer] } + } + + Column { + Row { + Button(onClick = { navController.navigate("home") }) { + Text("Back") + } + Button(onClick = { + messageStore?.loadBefore(buffer, limit) + }) { + Text("↑") + } + Button(onClick = { + messageStore?.loadAfter(buffer, limit) + }) { + Text("↓") + } + Button(onClick = { + messageStore?.loadAround(buffer, markerLine ?: MsgId(-1), limit) + }) { + Text("N") + } + Button(onClick = { + messageStore?.clear(buffer) + }) { + Text("Clr") + } + } + MessageList( + messages = messages, + listState = listState, + markerLine = markerLine ?: MsgId(-1), + buffer = 5, + onLoadAtStart = { messageStore?.loadBefore(buffer, limit) }, + onLoadAtEnd = { messageStore?.loadAfter(buffer, limit) } + ) + } +} + + diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/ListExtensions.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/ListExtensions.kt index 2ca165b979a1ef0ded6b91de69626325b917db31..8a1aa30c1d2be0dff2114c05d18602c8961c879e 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/ListExtensions.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/ListExtensions.kt @@ -9,3 +9,18 @@ fun <T> List<T>.getPrevious(index: Int): T? = fun <T> List<T>.getNext(index: Int): T? = getSafe(index + 1) + +@Suppress("NOTHING_TO_INLINE") +inline operator fun <T> List<T>.component6(): T = get(5) +@Suppress("NOTHING_TO_INLINE") +inline operator fun <T> List<T>.component7(): T = get(6) +@Suppress("NOTHING_TO_INLINE") +inline operator fun <T> List<T>.component8(): T = get(7) +@Suppress("NOTHING_TO_INLINE") +inline operator fun <T> List<T>.component9(): T = get(8) +@Suppress("NOTHING_TO_INLINE") +inline operator fun <T> List<T>.component10(): T = get(9) +@Suppress("NOTHING_TO_INLINE") +inline operator fun <T> List<T>.component11(): T = get(10) +@Suppress("NOTHING_TO_INLINE") +inline operator fun <T> List<T>.component12(): T = get(11) diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/FormatString.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/FormatString.kt index a8c8ea9ff65ef66a67f2c8a9612f5ee9180208c7..850c414a24b8fa41aac0d0bff40c86d4df466418 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/FormatString.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/FormatString.kt @@ -1,5 +1,7 @@ package de.justjanne.quasseldroid.util.format +import de.justjanne.quasseldroid.util.extensions.joinString + sealed class FormatString { data class FixedValue( val content: CharSequence @@ -10,26 +12,34 @@ sealed class FormatString { } data class FormatSpecifier( - val index: Int?, - val flags: String?, - val width: Int?, - val precision: Int?, - val time: Boolean, + val argumentIndex: Int? = null, + val flags: String? = null, + val width: Int? = null, + val precision: Int? = null, + val time: Boolean = false, val conversion: Char ) : FormatString() { - override fun toString(): String = listOfNotNull( - index?.let { "index=$index" }, - flags?.let { "flags='$flags'" }, - width?.let { "width=$width" }, - precision?.let { "precision=$precision" }, - "time=$time", - "conversion='$conversion'" - ).joinToString(", ", prefix = "FormatSpecifier(", postfix = ")") + override fun toString(): String = joinString(", ", "FormatSpecifier(", ")") { + if (argumentIndex != null) { + append("argumentIndex=$argumentIndex") + } + if (flags != null) { + append("flags=$flags") + } + if (width != null) { + append("width=$width") + } + if (precision != null) { + append("precision=$precision") + } + append("time=$time") + append("conversion=$conversion") + } fun toFormatSpecifier(ignoreFlags: Set<Char> = emptySet()) = buildString { append("%") - if (index != null) { - append(index) + if (argumentIndex != null) { + append(argumentIndex) append("$") } if (flags != null) { diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/DefaultContextualLifecycleObserver.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/DefaultContextualLifecycleObserver.kt index f6d44679d15803b6dd142fc6ed28ba9be0d30024..cb6a07646e2a07ebdb784db6946a8c645ae201b6 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/DefaultContextualLifecycleObserver.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/lifecycle/DefaultContextualLifecycleObserver.kt @@ -12,42 +12,42 @@ abstract class DefaultContextualLifecycleObserver : ContextualLifecycleObserver @CallSuper override fun onCreate(owner: Context) { require(statusInternal.compareAndSet(LifecycleStatus.DESTROYED, LifecycleStatus.CREATED)) { - "Unexpected lifecycle status: onCreate called, but status is not DESTROYED" + "Unexpected lifecycle status: onCreate called, but status is not DESTROYED: ${statusInternal.get()}" } } @CallSuper override fun onStart(owner: Context) { require(statusInternal.compareAndSet(LifecycleStatus.CREATED, LifecycleStatus.STARTED)) { - "Unexpected lifecycle status: onStart called, but status is not CREATED" + "Unexpected lifecycle status: onStart called, but status is not CREATED: ${statusInternal.get()}" } } @CallSuper override fun onResume(owner: Context) { require(statusInternal.compareAndSet(LifecycleStatus.STARTED, LifecycleStatus.RESUMED)) { - "Unexpected lifecycle status: onResume called, but status is not STARTED" + "Unexpected lifecycle status: onResume called, but status is not STARTED: ${statusInternal.get()}" } } @CallSuper override fun onPause(owner: Context) { require(statusInternal.compareAndSet(LifecycleStatus.RESUMED, LifecycleStatus.STARTED)) { - "Unexpected lifecycle status: onPause called, but status is not RESUMED" + "Unexpected lifecycle status: onPause called, but status is not RESUMED: ${statusInternal.get()}" } } @CallSuper override fun onStop(owner: Context) { require(statusInternal.compareAndSet(LifecycleStatus.STARTED, LifecycleStatus.CREATED)) { - "Unexpected lifecycle status: onStop called, but status is not RESUMED" + "Unexpected lifecycle status: onStop called, but status is not RESUMED: ${statusInternal.get()}" } } @CallSuper override fun onDestroy(owner: Context) { require(statusInternal.compareAndSet(LifecycleStatus.CREATED, LifecycleStatus.DESTROYED)) { - "Unexpected lifecycle status: onDestroy called, but status is not RESUMED" + "Unexpected lifecycle status: onDestroy called, but status is not RESUMED: ${statusInternal.get()}" } } } diff --git a/app/src/main/res/values/strings_messages.xml b/app/src/main/res/values/strings_messages.xml index d6c1be59ac4ff04eace35d245b7819a7f454c904..e49db9b8900a7949f50512847bb87d84333793f4 100644 --- a/app/src/main/res/values/strings_messages.xml +++ b/app/src/main/res/values/strings_messages.xml @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8" ?> <resources> - <string name="message_format_action">— %1$s%2$s %3$s</string> - <string name="message_format_notice">[%1$s%2$s] %3$s</string> - <string name="message_format_nick">%1$s%2$s is now known as %3$s%4$s</string> - <string name="message_format_nick_self">You are now known as %1$s%2$s</string> - <string name="message_format_mode">Mode %1$s by %2$s%3$s</string> - <string name="message_format_join">%1$s%2$s joined %3$s</string> - <string name="message_format_part_1">%1$s%2$s left</string> - <string name="message_format_part_2">%1$s%2$s left (%3$s)</string> - <string name="message_format_quit_1">%1$s%2$s quit</string> - <string name="message_format_quit_2">%1$s%2$s quit (%3$s)</string> - <string name="message_format_kick_1">%1$s was kicked by %2$s%3$s</string> - <string name="message_format_kick_2">%1$s was kicked by %2$s%3$s (%4$s)</string> - <string name="message_format_kill_1">%1$s was killed by %2$s%3$s</string> - <string name="message_format_kill_2">%1$s was killed by %2$s%3$s (%4$s)</string> + <string name="message_format_action">— %1$s %2$s</string> + <string name="message_format_notice">[%1$s] %2$s</string> + <string name="message_format_nick">%1$s is now known as %2$s</string> + <string name="message_format_nick_self">You are now known as %1$s</string> + <string name="message_format_mode">Mode %1$s by %2$s</string> + <string name="message_format_join">%1$s joined %2$s</string> + <string name="message_format_part_1">%1$s left</string> + <string name="message_format_part_2">%1$s left (%2$s)</string> + <string name="message_format_quit_1">%1$s quit</string> + <string name="message_format_quit_2">%1$s quit (%2$s)</string> + <string name="message_format_kick_1">%1$s was kicked by %2$s</string> + <string name="message_format_kick_2">%1$s was kicked by %2$s (%3$s)</string> + <string name="message_format_kill_1">%1$s was killed by %2$s</string> + <string name="message_format_kill_2">%1$s was killed by %2$s (%3$s)</string> </resources>