diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9500fe193331b416a8e88152cad684e03d99ab08..fed896206e1d5f321a79dfea03aa236ff3daa668 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,8 +14,8 @@ android:name=".MainActivity" android:exported="true" android:launchMode="singleTask" - android:windowSoftInputMode="adjustResize" - android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"> + android:theme="@style/Theme.AppCompat.DayNight.NoActionBar" + android:windowSoftInputMode="adjustResize"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.VIEW" /> 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 c9f670522e200b15c25cdb7a9327831f020f9807..d72031943389e9531229a36a5101ccc475803f39 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/messages/MessageStore.kt @@ -1,7 +1,11 @@ package de.justjanne.quasseldroid.messages +import androidx.collection.LruCache +import de.justjanne.bitflags.none import de.justjanne.libquassel.client.syncables.ClientBacklogManager import de.justjanne.libquassel.protocol.models.Message +import de.justjanne.libquassel.protocol.models.flags.MessageFlag +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.util.StateHolder @@ -41,51 +45,49 @@ class MessageStore( } }.launchIn(scope) - fun loadAround(bufferId: BufferId, messageId: MsgId) { + fun loadAround(bufferId: BufferId, messageId: MsgId, limit: Int) { scope.launch { state.update { messages -> val (before, after) = listOf( - async { backlogManager.backlog(bufferId, messageId) }, - async { backlogManager.backlogForward(bufferId, messageId) }, - ).awaitAll() + backlogManager.backlog(bufferId, last = messageId, limit = limit) + .mapNotNull { it.into<Message>() }, + backlogManager.backlogForward(bufferId, first = messageId, limit = limit - 1) + .mapNotNull { it.into<Message>() }, + ) val updated = MessageBuffer( atEnd = false, - messages = (before + after) - .mapNotNull { it.into<Message>() } - .sortedBy { it.messageId } + messages = (before + after).distinct().sortedBy { it.messageId } ) messages + Pair(bufferId, updated) } } } - fun loadBefore(bufferId: BufferId) { + fun loadBefore(bufferId: BufferId, limit: Int) { scope.launch { state.update { messages -> val buffer = messages[bufferId] ?: MessageBuffer(true, emptyList()) val messageId = buffer.messages.firstOrNull()?.messageId ?: MsgId(-1) - val data = backlogManager.backlog(bufferId, messageId) + val data = backlogManager.backlog(bufferId, last = messageId, limit = limit) .mapNotNull { it.into<Message>() } val updated = buffer.copy( - messages = (buffer.messages + data) - .sortedBy { it.messageId } + messages = (buffer.messages + data).distinct().sortedBy { it.messageId } ) messages + Pair(bufferId, updated) } } } - fun loadAfter(bufferId: BufferId) { + fun loadAfter(bufferId: BufferId, limit: Int) { scope.launch { state.update { messages -> val buffer = messages[bufferId] ?: MessageBuffer(true, emptyList()) val messageId = buffer.messages.lastOrNull()?.messageId ?: MsgId(-1) - val data = backlogManager.backlogForward(bufferId, messageId) + val data = backlogManager.backlogForward(bufferId, first = messageId, limit = limit) .mapNotNull { it.into<Message>() } val updated = buffer.copy( - messages = (buffer.messages + data) - .sortedBy { it.messageId } + messages = (buffer.messages + data).distinct().sortedBy { it.messageId } ) messages + Pair(bufferId, updated) } diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleBooleanProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleBooleanProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..3dca55290c23630e9f863ed511bc4e28f0d72f59 --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleBooleanProvider.kt @@ -0,0 +1,7 @@ +package de.justjanne.quasseldroid.sample + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +class SampleBooleanProvider : PreviewParameterProvider<Boolean> { + override val values = sequenceOf(false, true) +} diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleLocalDateProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleLocalDateProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..89a6de1d7d74ddb2968b6eee30a24dc127f9c520 --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleLocalDateProvider.kt @@ -0,0 +1,10 @@ +package de.justjanne.quasseldroid.sample + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import org.threeten.bp.LocalDate + +class SampleLocalDateProvider : PreviewParameterProvider<LocalDate> { + override val values = sequenceOf( + LocalDate.of(2022, 2, 28) + ) +} diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..5965a4375b0d15778e7ef7364ff0bc4835443437 --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessageProvider.kt @@ -0,0 +1,87 @@ +package de.justjanne.quasseldroid.sample + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.justjanne.bitflags.of +import de.justjanne.libquassel.protocol.models.BufferInfo +import de.justjanne.libquassel.protocol.models.Message +import de.justjanne.libquassel.protocol.models.flags.BufferType +import de.justjanne.libquassel.protocol.models.flags.MessageFlag +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 org.threeten.bp.Instant + +class SampleMessageProvider : PreviewParameterProvider<Message> { + override val values = sequenceOf( + Message( + messageId = MsgId(108062924), + bufferInfo = BufferInfo( + bufferId = BufferId(3746), + bufferName = "#quasseldroid", + networkId = NetworkId(4), + type = BufferType.of(BufferType.Channel) + ), + time = Instant.parse("2022-02-20T18:24:48.891Z"), + type = MessageType.of(MessageType.Quit), + sender = "CrazyBonz!~CrazyBonz@user/CrazyBonz", + senderPrefixes = "", + avatarUrl = "", + realName = "CrazyBonz", + content = "#quasseldroid", + flag = MessageFlag.of() + ), + Message( + messageId = MsgId(108063975), + bufferInfo = BufferInfo( + bufferId = BufferId(3746), + bufferName = "#quasseldroid", + networkId = NetworkId(4), + type = BufferType.of(BufferType.Channel) + ), + time = Instant.parse("2022-02-20T19:56:01.588Z"), + type = MessageType.of(MessageType.Plain), + sender = "winch!~AdminUser@185.14.29.13", + senderPrefixes = "", + avatarUrl = "", + realName = "Wincher,,,", + content = "Can i script some actions like in mIRC?", + flag = MessageFlag.of() + ), + Message( + messageId = MsgId(108064014), + bufferInfo = BufferInfo( + bufferId = BufferId(3746), + bufferName = "#quasseldroid", + networkId = NetworkId(4), + type = BufferType.of(BufferType.Channel) + ), + time = Instant.parse("2022-02-20T20:06:39.159Z"), + type = MessageType.of(MessageType.Quit), + sender = "mavhq!~quassel@mapp-14-b2-v4wan-161519-cust401.vm15.cable.virginm.net", + senderPrefixes = "", + avatarUrl = "", + realName = "mavhc", + content = "Quit: http://quassel-irc.org - Chat comfortably. Anywhere.", + flag = MessageFlag.of() + ), + Message( + messageId = MsgId(108064022), + bufferInfo = BufferInfo( + bufferId = BufferId(3746), + bufferName = "#quasseldroid", + networkId = NetworkId(4), + type = BufferType.of(BufferType.Channel) + ), + time = Instant.parse("2022-02-20T20:07:13.45Z"), + type = MessageType.of(MessageType.Join), + sender = "mavhq!~quassel@mapp-14-b2-v4wan-161519-cust401.vm15.cable.virginm.net", + senderPrefixes = "", + avatarUrl = "", + realName = "mavhc", + content = "#quasseldroid", + flag = MessageFlag.of() + ) + ) +} + diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessagesProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessagesProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..150a45f50b82868101c895af5597d8a740abd3cc --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleMessagesProvider.kt @@ -0,0 +1,10 @@ +package de.justjanne.quasseldroid.sample + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.justjanne.libquassel.protocol.models.Message + +class SampleMessagesProvider : PreviewParameterProvider<List<Message>> { + override val values = sequenceOf( + SampleMessageProvider().values.toList() + ) +} diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleSecurityLevelProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleSecurityLevelProvider.kt index ef595a103c909fa2f7fcb0d15dea9d598479ac3c..96cf4cec85e89210531d8ba89bfbc69f4eb2318d 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleSecurityLevelProvider.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/sample/SampleSecurityLevelProvider.kt @@ -3,7 +3,7 @@ package de.justjanne.quasseldroid.sample import androidx.compose.ui.tooling.preview.PreviewParameterProvider import de.justjanne.quasseldroid.model.SecurityLevel -class SampleSecurityLevelProvider: PreviewParameterProvider<SecurityLevel> { +class SampleSecurityLevelProvider : PreviewParameterProvider<SecurityLevel> { override val values = sequenceOf( SecurityLevel.SECURE, SecurityLevel.UNVERIFIED, diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/service/ClientSessionWrapper.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/service/ClientSessionWrapper.kt index a52a88fb688d4f23b726be9c2c42cd5c553addb6..428e82c52cc87b0d04700e81c0361d0e89aac04f 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/service/ClientSessionWrapper.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/service/ClientSessionWrapper.kt @@ -4,6 +4,6 @@ import de.justjanne.libquassel.client.session.ClientSession import de.justjanne.quasseldroid.messages.MessageStore data class ClientSessionWrapper( - val session: ClientSession, - val messages: MessageStore + val session: ClientSession, + val messages: MessageStore ) diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBackend.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBackend.kt index 6f2edb9d05f631263254dbbed2981a155e6e7ce7..fc63885b58f8d18f0036b76597bc85807b40f00a 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBackend.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBackend.kt @@ -5,7 +5,6 @@ import android.content.Context import android.content.ServiceConnection import android.os.IBinder import android.util.Log -import de.justjanne.libquassel.client.session.ClientSession import de.justjanne.libquassel.protocol.util.StateHolder import de.justjanne.libquassel.protocol.util.flatMap import de.justjanne.quasseldroid.BuildConfig diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBinder.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBinder.kt index 993619d8cf654e533fe933a8d400491877881dfe..d3cf4eb3e76a7c861a9653438f2faa56b0b5de71 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBinder.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/service/QuasselBinder.kt @@ -1,7 +1,6 @@ package de.justjanne.quasseldroid.service import android.os.Binder -import de.justjanne.libquassel.client.session.ClientSession import de.justjanne.libquassel.protocol.util.StateHolder import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/LoginView.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/LoginView.kt index d8a5cd6a2ab1c66f1f7c59479a6c4c92ede5ed2a..fa3b1a6369a9de858f2dcd809bcff3da0229674c 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/LoginView.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/LoginView.kt @@ -26,18 +26,21 @@ import java.net.InetSocketAddress @Preview(name = "Login", showBackground = true) @Composable -fun LoginView(onLogin: (ConnectionData) -> Unit = {}) { +fun LoginView( + default: ConnectionData? = null, + onLogin: (ConnectionData) -> Unit = {} +) { val (host, setHost) = rememberSaveable(stateSaver = TextFieldValueSaver) { - mutableStateOf(TextFieldValue()) + mutableStateOf(TextFieldValue(default?.address?.hostString ?: "")) } val (port, setPort) = rememberSaveable(stateSaver = TextFieldValueSaver) { - mutableStateOf(TextFieldValue("4242")) + mutableStateOf(TextFieldValue(default?.address?.port?.toString() ?: "4242")) } val (username, setUsername) = rememberSaveable(stateSaver = TextFieldValueSaver) { - mutableStateOf(TextFieldValue()) + mutableStateOf(TextFieldValue(default?.username ?: "")) } val (password, setPassword) = rememberSaveable(stateSaver = TextFieldValueSaver) { - mutableStateOf(TextFieldValue()) + mutableStateOf(TextFieldValue(default?.password ?: "")) } val focusManager = LocalFocusManager.current diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageView.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBaseView.kt similarity index 84% rename from app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageView.kt rename to app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBaseView.kt index c1424a357c50c082cc6f43a15ace87dba7d4f2f6..efe03af838ba32f88249adccfbbf3a8d27f8aaa5 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageView.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageBaseView.kt @@ -15,15 +15,12 @@ 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.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter 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.routes.SampleMessageProvider import de.justjanne.quasseldroid.ui.theme.QuasselTheme import de.justjanne.quasseldroid.ui.theme.Typography import irc.SenderColorUtil @@ -33,20 +30,6 @@ import org.threeten.bp.format.FormatStyle private val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) -@Preview(name = "Message", showBackground = true) -@Composable -fun MessageView( - @PreviewParameter(SampleMessageProvider::class) - message: Message -) { - MessageBaseView(message, false, 32.dp) { - Text( - message.content, - style = Typography.body2, - ) - } -} - @Composable fun MessageBaseView( message: Message, @@ -57,9 +40,9 @@ fun MessageBaseView( val nick = HostmaskHelper.nick(message.sender) val senderColor = QuasselTheme.sender.colors[SenderColorUtil.senderColor(nick)] - Row { + Row(modifier = Modifier.padding(2.dp)) { if (!followUp) { - AvatarIcon(nick, null, modifier = Modifier.padding(2.dp)) + AvatarIcon(nick, null, modifier = Modifier.padding(vertical = 2.dp)) Spacer(Modifier.width(4.dp)) } else { Spacer(Modifier.width(avatarSize + 8.dp)) diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageDayChangeView.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageDayChangeView.kt new file mode 100644 index 0000000000000000000000000000000000000000..953f9e9f3d11c9fc2411a945a5dac6ec63f9c994 --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageDayChangeView.kt @@ -0,0 +1,92 @@ +package de.justjanne.quasseldroid.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.ContentAlpha +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import de.justjanne.quasseldroid.ui.theme.QuasselTheme +import de.justjanne.quasseldroid.ui.theme.Typography +import org.threeten.bp.LocalDate +import org.threeten.bp.format.DateTimeFormatter +import org.threeten.bp.format.FormatStyle + +private val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) + +@Preview(name = "Day Change", showBackground = true) +@Composable +private fun MessageDayChangePreview() { + Column { + MessageDayChangeView(LocalDate.of(2018, 9, 7), isNew = false) + MessageDayChangeView(LocalDate.of(2018, 9, 7), isNew = true) + } +} + +@Composable +fun MessageDayChangeView( + date: LocalDate, + isNew: Boolean +) { + val foregroundColor = + if (isNew) QuasselTheme.security.insecure + else MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled) + + Row(modifier = Modifier.padding(vertical = 4.dp)) { + Spacer(Modifier.width(8.dp)) + Divider( + color = foregroundColor, + modifier = Modifier + .weight(1.0f) + .height(1.dp) + .align(Alignment.CenterVertically) + ) + Spacer(Modifier.width(4.dp)) + Text( + date.format(formatter), + modifier = Modifier + .align(Alignment.CenterVertically), + style = Typography.body2, + fontWeight = FontWeight.Medium, + ) + Spacer(Modifier.width(4.dp)) + Row( + modifier = Modifier + .weight(1.0f) + .align(Alignment.CenterVertically) + ) { + Divider( + color = foregroundColor, + modifier = Modifier + .weight(1.0f) + .height(1.dp) + .align(Alignment.CenterVertically) + ) + if (isNew) { + Spacer(Modifier.width(4.dp)) + Text( + "New", + modifier = Modifier + .align(Alignment.CenterVertically), + color = QuasselTheme.security.insecure, + style = Typography.body2, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + ) + } + } + Spacer(Modifier.width(8.dp)) + } +} + 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 new file mode 100644 index 0000000000000000000000000000000000000000..ae018dfd7fd07b832a3714e6be574137bc8a5d0e --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/MessageList.kt @@ -0,0 +1,65 @@ +package de.justjanne.quasseldroid.ui.components + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import de.justjanne.libquassel.protocol.models.Message +import de.justjanne.libquassel.protocol.models.ids.MsgId +import de.justjanne.quasseldroid.sample.SampleMessagesProvider +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 org.threeten.bp.ZoneId + +@Preview(name = "Messages", showBackground = true) +@Composable +fun MessageList( + @PreviewParameter(SampleMessagesProvider::class) + messages: List<Message>, + listState: LazyListState = rememberLazyListState(), + markerLine: MsgId = MsgId(-1), + buffer: Int = 0, + onLoadAtStart: () -> Unit = { }, + onLoadAtEnd: () -> Unit = { }, +) { + LazyColumn(state = listState) { + itemsIndexed(messages, key = { _, item -> item.messageId }) { index, message -> + val prev = messages.getPrevious(index) + val prevDate = prev?.time?.atZone(ZoneId.systemDefault())?.toLocalDate() + val messageDate = message.time.atZone(ZoneId.systemDefault()).toLocalDate() + + val followUp = prev != null && + message.sender == prev.sender && + message.senderPrefixes == prev.senderPrefixes && + message.realName == prev.realName && + message.avatarUrl == prev.avatarUrl + + val isNew = prev != null && + prev.messageId <= markerLine && + message.messageId > markerLine + + if (prevDate == null || !messageDate.isEqual(prevDate)) { + MessageDayChangeView(messageDate, isNew) + } else if (isNew) { + NewMessageView() + } + + MessageBaseView(message, followUp, 32.dp) { + Text( + message.content, + style = Typography.body2, + ) + } + } + } + + listState.OnTopReached(buffer = buffer, onLoadMore = onLoadAtStart) + listState.OnBottomReached(buffer = buffer, onLoadMore = onLoadAtEnd) +} diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/NewMessageView.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/NewMessageView.kt new file mode 100644 index 0000000000000000000000000000000000000000..0645ae772865aeb5a18ac62a9b4a2147dc14c9f2 --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/NewMessageView.kt @@ -0,0 +1,44 @@ +package de.justjanne.quasseldroid.ui.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import de.justjanne.quasseldroid.ui.theme.QuasselTheme +import de.justjanne.quasseldroid.ui.theme.Typography + +@Preview(name = "New Message", showBackground = true) +@Composable +fun NewMessageView() { + Row(modifier = Modifier.padding(vertical = 4.dp)) { + Spacer(Modifier.width(8.dp)) + Divider( + color = QuasselTheme.security.insecure, + modifier = Modifier + .height(1.dp) + .weight(1.0f) + .align(Alignment.CenterVertically), + ) + Spacer(Modifier.width(4.dp)) + Text( + "New", + modifier = Modifier + .align(Alignment.CenterVertically), + color = QuasselTheme.security.insecure, + style = Typography.body2, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + ) + Spacer(Modifier.width(8.dp)) + } +} diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/PasswordTextField.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/PasswordTextField.kt index fba9e2fdbf112b744f1a1c527983d4366745c8ca..ac3c389fcc6379ee141522c1cd5447f2674757a2 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/PasswordTextField.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/components/PasswordTextField.kt @@ -18,14 +18,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview -import de.justjanne.quasseldroid.R @Preview(name = "PasswordTextField", showBackground = true) @Composable 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 e6ac4bd99df36f9a93c6c768bae8afb2c704893f..757b3118b93cedc5c8e2004e9326a8d79a0e5d56 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 @@ -35,7 +35,7 @@ fun AvatarIcon( size: Dp = 32.dp ) { val senderColor = QuasselTheme.sender.colors[SenderColorUtil.senderColor(nick)] - val initial = nick.asSequence().map { it.uppercase(Locale.ENGLISH) }.first() + val initial = nick.firstOrNull()?.uppercase(Locale.ENGLISH) ?: "?" val fontSize = with(LocalDensity.current) { (size.toPx() * 0.67f).toSp() } Surface( 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 09ee1010faed3e51ef9e5b80037ba306c2764672..0b9d1123c47e1cb739b763f025f868e1cbbf5573 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,25 +1,34 @@ package de.justjanne.quasseldroid.ui.routes +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +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.MessageView +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.BufferIdSaver +import de.justjanne.quasseldroid.util.saver.TextFieldValueSaver import kotlinx.coroutines.flow.map +private const val limit = 20 + @Composable fun HomeRoute(backend: QuasselBackend, navController: NavController) { val session = rememberFlow(null) { @@ -27,18 +36,40 @@ fun HomeRoute(backend: QuasselBackend, navController: NavController) { .mapNullable { it.session } } - val (buffer, setBuffer) = rememberSaveable(stateSaver = BufferIdSaver) { - mutableStateOf(BufferId(-1)) + 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 } } val messages: List<Message> = rememberFlow(emptyList()) { backend.flow() .mapNullable { it.messages } .flatMap() - .mapNullable { it[buffer] } + .mapNullable { it[bufferId] } .map { it?.messages.orEmpty() } } + val markerLine: MsgId? = rememberFlow(null) { + backend.flow() + .mapNullable { it.session } + .flatMap() + .mapNullable { it.bufferSyncer } + .flatMap() + .mapNullable { it.markerLines[bufferId] } + } + val initStatus = rememberFlow(null) { backend.flow() .mapNullable { it.session } @@ -47,26 +78,50 @@ fun HomeRoute(backend: QuasselBackend, navController: NavController) { } val context = LocalContext.current + val buttonScrollState = rememberScrollState() + Column { Text("Side: ${session?.side}") if (initStatus != null) { val done = initStatus.total - initStatus.waiting.size Text("Init: ${initStatus.started} $done/ ${initStatus.total}") } - Button(onClick = { navController.navigate("coreInfo") }) { - Text("Core Info") - } - Button(onClick = { - backend.disconnect(context) - navController.navigate("login") - }) { - Text("Disconnect") - } - LazyColumn { - items(messages, key = Message::messageId) { - MessageView(it) + 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("…") } } + 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) } + ) } } diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/LoginRoute.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/LoginRoute.kt index 789c050790c866e07f4230e71779dcaf485ce233..e40af49613b49a8bd0d53bc4a552303f4743f5d2 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/LoginRoute.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/LoginRoute.kt @@ -1,17 +1,49 @@ package de.justjanne.quasseldroid.ui.routes +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext +import androidx.core.content.edit import androidx.navigation.NavController +import de.justjanne.quasseldroid.service.ConnectionData import de.justjanne.quasseldroid.service.QuasselBackend import de.justjanne.quasseldroid.ui.components.LoginView +import java.net.InetSocketAddress @Composable fun LoginRoute(backend: QuasselBackend, navController: NavController) { val context = LocalContext.current - LoginView(onLogin = { - if (backend.login(context, it)) { - navController.navigate("home") - } - }) + LoginView(default = loadConnectionData(context)) { + if (backend.login(context, it)) { + navController.navigate("home") + saveConnectionData(context, it) + } + } +} + +fun loadConnectionData(context: Context): ConnectionData? { + val sharedPreferences = context.getSharedPreferences("login", Context.MODE_PRIVATE) + return ConnectionData( + address = InetSocketAddress.createUnresolved( + sharedPreferences.getString("host", null) + ?: return null, + sharedPreferences.getInt("port", 0) + .takeIf { it > 0 } + ?: return null, + ), + username = sharedPreferences.getString("username", null) + ?: return null, + password = sharedPreferences.getString("password", null) + ?: return null + ) +} + +fun saveConnectionData(context: Context, connectionData: ConnectionData) { + val sharedPreferences = context.getSharedPreferences("login", Context.MODE_PRIVATE) + sharedPreferences.edit { + putString("host", connectionData.address.hostString) + putInt("port", connectionData.address.port) + putString("username", connectionData.username) + putString("password", connectionData.password) + } } diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/SampleMessageProvider.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/SampleMessageProvider.kt deleted file mode 100644 index ff30e9f8fc0ef879e35d45aceaf9d66672af8354..0000000000000000000000000000000000000000 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/ui/routes/SampleMessageProvider.kt +++ /dev/null @@ -1,86 +0,0 @@ -package de.justjanne.quasseldroid.ui.routes - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import de.justjanne.bitflags.of -import de.justjanne.libquassel.protocol.models.BufferInfo -import de.justjanne.libquassel.protocol.models.Message -import de.justjanne.libquassel.protocol.models.flags.BufferType -import de.justjanne.libquassel.protocol.models.flags.MessageFlag -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 org.threeten.bp.Instant - -class SampleMessageProvider : PreviewParameterProvider<Message> { - override val values = sequenceOf( - Message( - messageId = MsgId(108062924), - bufferInfo = BufferInfo( - bufferId = BufferId(3746), - bufferName = "#quasseldroid", - networkId = NetworkId(4), - type = BufferType.of(BufferType.Channel) - ), - time = Instant.parse("2022-02-20T18:24:48.891Z"), - type = MessageType.of(MessageType.Quit), - sender = "CrazyBonz!~CrazyBonz@user/CrazyBonz", - senderPrefixes = "", - avatarUrl = "", - realName = "CrazyBonz", - content = "#quasseldroid", - flag = MessageFlag.of() - ), - Message( - messageId = MsgId(108063975), - bufferInfo = BufferInfo( - bufferId = BufferId(3746), - bufferName = "#quasseldroid", - networkId = NetworkId(4), - type = BufferType.of(BufferType.Channel) - ), - time = Instant.parse("2022-02-20T19:56:01.588Z"), - type = MessageType.of(MessageType.Plain), - sender = "winch!~AdminUser@185.14.29.13", - senderPrefixes = "", - avatarUrl = "", - realName = "Wincher,,,", - content = "Can i script some actions like in mIRC?", - flag = MessageFlag.of() - ), - Message( - messageId = MsgId(108064014), - bufferInfo = BufferInfo( - bufferId = BufferId(3746), - bufferName = "#quasseldroid", - networkId = NetworkId(4), - type = BufferType.of(BufferType.Channel) - ), - time = Instant.parse("2022-02-20T20:06:39.159Z"), - type = MessageType.of(MessageType.Quit), - sender = "mavhq!~quassel@mapp-14-b2-v4wan-161519-cust401.vm15.cable.virginm.net", - senderPrefixes = "", - avatarUrl = "", - realName = "mavhc", - content = "Quit: http://quassel-irc.org - Chat comfortably. Anywhere.", - flag = MessageFlag.of() - ), - Message( - messageId = MsgId(108064022), - bufferInfo = BufferInfo( - bufferId = BufferId(3746), - bufferName = "#quasseldroid", - networkId = NetworkId(4), - type = BufferType.of(BufferType.Channel) - ), - time = Instant.parse("2022-02-20T20:07:13.45Z"), - type = MessageType.of(MessageType.Join), - sender = "mavhq!~quassel@mapp-14-b2-v4wan-161519-cust401.vm15.cable.virginm.net", - senderPrefixes = "", - avatarUrl = "", - realName = "mavhc", - content = "#quasseldroid", - flag = MessageFlag.of() - ) - ) -} diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/LazyListStateExtensions.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/LazyListStateExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..1deb227bc5f68597bdeb072e50773a151d4583bd --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/LazyListStateExtensions.kt @@ -0,0 +1,50 @@ +package de.justjanne.quasseldroid.util.extensions + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow + +@Composable +fun LazyListState.OnBottomReached( + buffer: Int = 0, + onLoadMore: () -> Unit +) { + require(buffer >= 0) { "buffer cannot be negative, but was $buffer" } + + val shouldLoadMore = remember { + derivedStateOf { + val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull() + ?: return@derivedStateOf true + lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - buffer + } + } + + LaunchedEffect(shouldLoadMore) { + snapshotFlow { shouldLoadMore.value } + .collect { if (it) onLoadMore() } + } +} + +@Composable +fun LazyListState.OnTopReached( + buffer: Int = 0, + onLoadMore: () -> Unit +) { + require(buffer >= 0) { "buffer cannot be negative, but was $buffer" } + + val shouldLoadMore = remember { + derivedStateOf { + val lastVisibleItem = layoutInfo.visibleItemsInfo.firstOrNull() + ?: return@derivedStateOf true + lastVisibleItem.index <= buffer + } + } + + LaunchedEffect(shouldLoadMore) { + snapshotFlow { shouldLoadMore.value } + .collect { if (it) onLoadMore() } + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..2ca165b979a1ef0ded6b91de69626325b917db31 --- /dev/null +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/extensions/ListExtensions.kt @@ -0,0 +1,11 @@ +package de.justjanne.quasseldroid.util.extensions + +fun <T> List<T>.getSafe(index: Int): T? = + if (index !in 0..size) null + else get(index) + +fun <T> List<T>.getPrevious(index: Int): T? = + getSafe(index - 1) + +fun <T> List<T>.getNext(index: Int): T? = + getSafe(index + 1) diff --git a/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/SenderColorUtilTest.kt b/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/SenderColorUtilTest.kt index 79fc84a549bcc15d64a1f2746f7081b1dd26ddba..e26136bb1c371cb5b6a3555807ed74ad953f901e 100644 --- a/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/SenderColorUtilTest.kt +++ b/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/SenderColorUtilTest.kt @@ -1,7 +1,7 @@ package de.kuschku.justjanne.quasseldroid.util.irc import irc.SenderColorUtil -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class SenderColorUtilTest { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4258909f6bfd691639f75c909be300225557c341..6594147bcbd9fac76ccc084748be18c04d14569e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -libquassel = "0.9.0" +libquassel = "0.9.2" androidx-activity = "1.4.0" androidx-appcompat = "1.4.1" androidx-compose = "1.1.1"