talk-android/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt
Marcel Hibbe 59ced8843e
fix to not show null if no last message of thread exists
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-07-17 14:03:39 +02:00

278 lines
9.1 KiB
Kotlin

/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.threadsoverview
import android.content.Intent
import android.os.Bundle
import android.text.format.DateUtils
import android.util.Log
import androidx.activity.compose.setContent
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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider
import autodagger.AutoInjector
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.ChatActivity.Companion.TAG
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.components.StandardAppBar
import com.nextcloud.talk.contacts.loadImage
import com.nextcloud.talk.models.json.threads.ThreadInfo
import com.nextcloud.talk.threadsoverview.components.ThreadRow
import com.nextcloud.talk.threadsoverview.viewmodels.ThreadsOverviewViewModel
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class ThreadsOverviewActivity : BaseActivity() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject
lateinit var ncApi: NcApi
@Inject
lateinit var userManager: UserManager
lateinit var threadsOverviewViewModel: ThreadsOverviewViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
threadsOverviewViewModel = ViewModelProvider(
this,
viewModelFactory
)[ThreadsOverviewViewModel::class.java]
val colorScheme = viewThemeUtils.getColorScheme(this)
val extras: Bundle? = intent.extras
val roomToken = extras?.getString(KEY_ROOM_TOKEN).orEmpty()
threadsOverviewViewModel.init(roomToken)
setContent {
val backgroundColor = colorResource(id = R.color.bg_default)
MaterialTheme(
colorScheme = colorScheme
) {
ColoredStatusBar()
Scaffold(
modifier = Modifier
.statusBarsPadding(),
topBar = {
StandardAppBar(
title = stringResource(R.string.recent_threads),
null
)
},
content = { paddingValues ->
val uiState by threadsOverviewViewModel.threadsListState.collectAsState()
Column(
Modifier
.padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp)
.background(backgroundColor)
.fillMaxSize()
) {
ThreadsOverviewScreen(
uiState,
roomToken,
onThreadClick = { roomToken, threadId ->
navigateToChatActivity(roomToken, threadId)
},
threadsOverviewViewModel
)
}
}
)
}
}
}
private fun navigateToChatActivity(roomToken: String, threadId: Int) {
val bundle = Bundle()
bundle.putString(KEY_ROOM_TOKEN, roomToken)
bundle.putLong(KEY_THREAD_ID, threadId.toLong())
val chatIntent = Intent(context, ChatActivity::class.java)
chatIntent.putExtras(bundle)
startActivity(chatIntent)
}
override fun onResume() {
super.onResume()
supportActionBar?.show()
}
companion object {
val TAG = ThreadsOverviewActivity::class.java.simpleName
}
}
@Composable
fun ThreadsOverviewScreen(
uiState: ThreadsOverviewViewModel.ThreadsListUiState,
roomToken: String,
onThreadClick: (roomToken: String, threadId: Int) -> Unit,
threadsOverviewViewModel: ThreadsOverviewViewModel
) {
when (val state = uiState) {
is ThreadsOverviewViewModel.ThreadsListUiState.None -> {
LoadingIndicator()
}
is ThreadsOverviewViewModel.ThreadsListUiState.Success -> {
ThreadsList(
threads = state.threadsList!!,
roomToken = roomToken,
onThreadClick = onThreadClick,
threadsOverviewViewModel
)
}
is ThreadsOverviewViewModel.ThreadsListUiState.Error -> {
Log.e(TAG, "Error when retrieving threads", uiState.exception)
ErrorView(message = stringResource(R.string.nc_common_error_sorry))
}
}
}
@Composable
fun ThreadsList(
threads: List<ThreadInfo>,
roomToken: String,
onThreadClick: (roomToken: String, threadId: Int) -> Unit,
threadsOverviewViewModel: ThreadsOverviewViewModel
) {
val context = LocalContext.current
if (threads.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text("No threads found.")
}
return
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = threads,
key = { threadInfo -> threadInfo.thread!!.id }
) { threadInfo ->
val imageUri = ApiUtils.getUrlForAvatar(
threadsOverviewViewModel.currentUser.baseUrl,
threadInfo.first?.actorId,
true
)
val errorPlaceholderImage: Int = R.drawable.account_circle_96dp
val imageRequest = loadImage(imageUri, context, errorPlaceholderImage)
ThreadRow(
roomToken = roomToken,
threadId = threadInfo.thread!!.id,
firstLine = threadInfo.first?.message.orEmpty(),
secondLine = threadInfo.last?.message.orEmpty(),
date = getLastActivityDate(threadInfo), // TODO: replace with value from api when available
imageRequest = imageRequest,
onClick = onThreadClick
)
}
}
}
private fun getLastActivityDate(threadInfo: ThreadInfo): String {
val MILLIES = 1000L
val lastActivityTimestamp = threadInfo.last?.timestamp
?: threadInfo.first?.timestamp
?: 0
val lastActivityDate = DateUtils.getRelativeTimeSpanString(
lastActivityTimestamp.times(MILLIES),
System.currentTimeMillis(),
0,
DateUtils.FORMAT_ABBREV_RELATIVE
).toString()
return lastActivityDate
}
@Composable
fun LoadingIndicator() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
@Composable
fun ErrorView(message: String) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(text = message, color = MaterialTheme.colorScheme.error)
}
}
// @Preview(showBackground = true)
// @Composable
// fun PreviewLoadingIndicator() {
// MaterialTheme {
// LoadingIndicator()
// }
// }
//
// @Preview(showBackground = true)
// @Composable
// fun PreviewErrorView() {
// MaterialTheme {
// ErrorView("This is a preview error message.")
// }
// }