From 829d0091bb16c6fcc998e91d726377229d1a7ca6 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Wed, 30 Aug 2023 13:17:11 +0530 Subject: [PATCH] Initial support to reply on threads Signed-off-by: Aayush Gupta --- .../aayush/relabs/network/XenforoInterface.kt | 9 ++ .../relabs/network/XenforoRepository.kt | 20 ++++ .../relabs/network/data/post/PostReply.kt | 6 + .../aayush/relabs/ui/navigation/NavGraph.kt | 11 ++ .../io/aayush/relabs/ui/navigation/Screen.kt | 10 ++ .../relabs/ui/screens/login/LoginScreen.kt | 1 - .../relabs/ui/screens/reply/ReplyScreen.kt | 112 ++++++++++++++++++ .../relabs/ui/screens/reply/ReplyViewModel.kt | 27 +++++ .../relabs/ui/screens/thread/ThreadScreen.kt | 19 +++ app/src/main/res/drawable/ic_quick_reply.xml | 10 ++ app/src/main/res/values/strings.xml | 3 + 11 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/io/aayush/relabs/network/data/post/PostReply.kt create mode 100644 app/src/main/java/io/aayush/relabs/ui/screens/reply/ReplyScreen.kt create mode 100644 app/src/main/java/io/aayush/relabs/ui/screens/reply/ReplyViewModel.kt create mode 100644 app/src/main/res/drawable/ic_quick_reply.xml diff --git a/app/src/main/java/io/aayush/relabs/network/XenforoInterface.kt b/app/src/main/java/io/aayush/relabs/network/XenforoInterface.kt index 3ba5ea6..cf4d79d 100644 --- a/app/src/main/java/io/aayush/relabs/network/XenforoInterface.kt +++ b/app/src/main/java/io/aayush/relabs/network/XenforoInterface.kt @@ -3,11 +3,13 @@ package io.aayush.relabs.network import io.aayush.relabs.network.data.alert.Alerts import io.aayush.relabs.network.data.conversation.Conversations import io.aayush.relabs.network.data.node.Nodes +import io.aayush.relabs.network.data.post.PostReply import io.aayush.relabs.network.data.thread.ThreadInfo import io.aayush.relabs.network.data.thread.Threads import io.aayush.relabs.network.data.user.Me import retrofit2.Response import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query @@ -64,4 +66,11 @@ interface XenforoInterface { @Query("with_last_post") with_last_post: Boolean? = null, @Query("order") order: String? = null ): Response + + @POST("posts") + suspend fun postReply( + @Query("thread_id") threadID: Int, + @Query("message") message: String, + @Query("attachment_key") attachmentKey: String? = null + ): Response } diff --git a/app/src/main/java/io/aayush/relabs/network/XenforoRepository.kt b/app/src/main/java/io/aayush/relabs/network/XenforoRepository.kt index abdae1d..8c9246a 100644 --- a/app/src/main/java/io/aayush/relabs/network/XenforoRepository.kt +++ b/app/src/main/java/io/aayush/relabs/network/XenforoRepository.kt @@ -4,6 +4,7 @@ import android.util.Log import io.aayush.relabs.network.data.alert.Alerts import io.aayush.relabs.network.data.conversation.Conversations import io.aayush.relabs.network.data.node.Nodes +import io.aayush.relabs.network.data.post.PostReply import io.aayush.relabs.network.data.thread.ThreadInfo import io.aayush.relabs.network.data.thread.Threads import io.aayush.relabs.network.data.user.Me @@ -170,4 +171,23 @@ class XenforoRepository @Inject constructor( null } } + + suspend fun postReply( + threadID: Int, + message: String, + attachmentKey: String? = null + ): PostReply? { + return try { + val response = xenforoInterface.postReply(threadID, message, attachmentKey) + if (response.isSuccessful) { + response.body() + } else { + Log.i(TAG, "Status: ${response.code()}, ${response.errorBody()}") + null + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to post reply!", exception) + null + } + } } diff --git a/app/src/main/java/io/aayush/relabs/network/data/post/PostReply.kt b/app/src/main/java/io/aayush/relabs/network/data/post/PostReply.kt new file mode 100644 index 0000000..f4a7827 --- /dev/null +++ b/app/src/main/java/io/aayush/relabs/network/data/post/PostReply.kt @@ -0,0 +1,6 @@ +package io.aayush.relabs.network.data.post + +data class PostReply( + val success: Boolean = false, + val post: Post = Post() +) diff --git a/app/src/main/java/io/aayush/relabs/ui/navigation/NavGraph.kt b/app/src/main/java/io/aayush/relabs/ui/navigation/NavGraph.kt index de8fcd6..bf24845 100644 --- a/app/src/main/java/io/aayush/relabs/ui/navigation/NavGraph.kt +++ b/app/src/main/java/io/aayush/relabs/ui/navigation/NavGraph.kt @@ -20,6 +20,7 @@ import io.aayush.relabs.ui.screens.alerts.AlertsScreen import io.aayush.relabs.ui.screens.home.HomeScreen import io.aayush.relabs.ui.screens.login.LoginScreen import io.aayush.relabs.ui.screens.news.NewsScreen +import io.aayush.relabs.ui.screens.reply.ReplyScreen import io.aayush.relabs.ui.screens.settings.SettingsScreen import io.aayush.relabs.ui.screens.thread.ThreadScreen @@ -51,5 +52,15 @@ fun SetupNavGraph( ) { ThreadScreen(navHostController, it.arguments!!.getInt(NavArg.THREAD_ID.name)) } + composable( + route = Screen.Reply.route, + arguments = listOf( + navArgument(NavArg.THREAD_ID.name) { + type = NavType.IntType + } + ) + ) { + ReplyScreen(navHostController, it.arguments!!.getInt(NavArg.THREAD_ID.name)) + } } } diff --git a/app/src/main/java/io/aayush/relabs/ui/navigation/Screen.kt b/app/src/main/java/io/aayush/relabs/ui/navigation/Screen.kt index 20b5cf2..1cb513e 100644 --- a/app/src/main/java/io/aayush/relabs/ui/navigation/Screen.kt +++ b/app/src/main/java/io/aayush/relabs/ui/navigation/Screen.kt @@ -49,4 +49,14 @@ sealed class Screen(val route: String, @StringRes val title: Int, @DrawableRes v return this.route.replace("{${NavArg.THREAD_ID.name}}", id.toString()) } } + + data object Reply : Screen( + route = "reply_screen/{${NavArg.THREAD_ID.name}}", + title = R.string.reply, + icon = R.drawable.ic_quick_reply + ) { + fun withID(id: Int): String { + return this.route.replace("{${NavArg.THREAD_ID.name}}", id.toString()) + } + } } diff --git a/app/src/main/java/io/aayush/relabs/ui/screens/login/LoginScreen.kt b/app/src/main/java/io/aayush/relabs/ui/screens/login/LoginScreen.kt index 332430b..90cd5b1 100644 --- a/app/src/main/java/io/aayush/relabs/ui/screens/login/LoginScreen.kt +++ b/app/src/main/java/io/aayush/relabs/ui/screens/login/LoginScreen.kt @@ -39,7 +39,6 @@ fun LoginScreen( navHostController: NavHostController, viewModel: LoginScreenViewModel = hiltViewModel() ) { - val startActivityForResult = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult(), onResult = { diff --git a/app/src/main/java/io/aayush/relabs/ui/screens/reply/ReplyScreen.kt b/app/src/main/java/io/aayush/relabs/ui/screens/reply/ReplyScreen.kt new file mode 100644 index 0000000..cac048b --- /dev/null +++ b/app/src/main/java/io/aayush/relabs/ui/screens/reply/ReplyScreen.kt @@ -0,0 +1,112 @@ +package io.aayush.relabs.ui.screens.reply + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import io.aayush.relabs.network.data.post.PostReply +import io.aayush.relabs.ui.navigation.Screen +import kotlinx.coroutines.flow.collectLatest + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun ReplyScreen( + navHostController: NavHostController, + threadID: Int, + viewModel: ReplyViewModel = hiltViewModel() +) { + var text by remember { mutableStateOf("") } + var posting by remember { mutableStateOf(false) } + + val postReply: PostReply? by viewModel.postReply.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = postReply) { + snapshotFlow { postReply }.collectLatest { + if (it != null && it.success) { + navHostController.navigateUp() + } else { + posting = false + } + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = stringResource(id = Screen.Reply.title)) }, + navigationIcon = { + IconButton(onClick = { navHostController.navigateUp() }) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = "" + ) + } + } + ) + }, + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text(text = stringResource(id = Screen.Reply.title)) }, + icon = { + if (posting) { + CircularProgressIndicator() + } else { + Image( + painter = painterResource(id = Screen.Reply.icon), + contentDescription = "" + ) + } + }, + onClick = { + if (!posting && text.isNotBlank()) { + posting = true + viewModel.postReply(threadID, text) + } + } + ) + } + ) { + Column( + modifier = Modifier + .padding(it) + .fillMaxSize() + ) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.5f) + .padding(20.dp), + value = text, + onValueChange = { newText -> text = newText }, + enabled = !posting + ) + } + } +} diff --git a/app/src/main/java/io/aayush/relabs/ui/screens/reply/ReplyViewModel.kt b/app/src/main/java/io/aayush/relabs/ui/screens/reply/ReplyViewModel.kt new file mode 100644 index 0000000..38c1206 --- /dev/null +++ b/app/src/main/java/io/aayush/relabs/ui/screens/reply/ReplyViewModel.kt @@ -0,0 +1,27 @@ +package io.aayush.relabs.ui.screens.reply + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import io.aayush.relabs.network.XenforoRepository +import io.aayush.relabs.network.data.post.PostReply +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ReplyViewModel @Inject constructor( + private val xenforoRepository: XenforoRepository +) : ViewModel() { + + private val _postReply = MutableStateFlow(PostReply()) + val postReply = _postReply.asStateFlow() + + fun postReply(threadID: Int, message: String) { + viewModelScope.launch(Dispatchers.IO) { + _postReply.value = xenforoRepository.postReply(threadID, message) + } + } +} diff --git a/app/src/main/java/io/aayush/relabs/ui/screens/thread/ThreadScreen.kt b/app/src/main/java/io/aayush/relabs/ui/screens/thread/ThreadScreen.kt index 0d4ec78..d7151db 100644 --- a/app/src/main/java/io/aayush/relabs/ui/screens/thread/ThreadScreen.kt +++ b/app/src/main/java/io/aayush/relabs/ui/screens/thread/ThreadScreen.kt @@ -1,6 +1,7 @@ package io.aayush.relabs.ui.screens.thread import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -18,6 +19,7 @@ import androidx.compose.material.icons.filled.KeyboardArrowLeft import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold @@ -30,6 +32,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -37,6 +41,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import io.aayush.relabs.network.data.thread.ThreadInfo import io.aayush.relabs.ui.components.PostItem +import io.aayush.relabs.ui.navigation.Screen import kotlinx.coroutines.launch @Composable @@ -72,6 +77,20 @@ fun ThreadScreen( } } ) + }, + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text(text = stringResource(id = Screen.Reply.title)) }, + icon = { + Image( + painter = painterResource(id = Screen.Reply.icon), + contentDescription = "" + ) + }, + onClick = { + navHostController.navigate(Screen.Reply.withID(threadID)) + } + ) } ) { Column(modifier = Modifier.padding(it)) { diff --git a/app/src/main/res/drawable/ic_quick_reply.xml b/app/src/main/res/drawable/ic_quick_reply.xml new file mode 100644 index 0000000..a0492a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_quick_reply.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb5ea7f..47a445f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,4 +40,7 @@ \u2022OP\u2022 %1$d people reacted to this post You and %1$d people reacted to this post + + + Reply