Merge pull request #4830 from nextcloud/test_notifications

Test notifications
This commit is contained in:
Sowjanya Kota 2025-04-28 09:15:31 +02:00 committed by GitHub
commit 1c6cd78fdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 378 additions and 13 deletions

View File

@ -17,6 +17,7 @@ import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.participants.AddParticipantOverall
import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.models.json.participants.TalkBanOverall
import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
import okhttp3.MultipartBody
import okhttp3.RequestBody
@ -238,6 +239,12 @@ interface NcApiCoroutines {
@Url url: String
): UserAbsenceOverall
@POST
suspend fun testPushNotifications(
@Header("Authorization") authorization: String,
@Url url: String
): TestNotificationOverall
@GET
suspend fun getContextOfChatMessage(
@Header("Authorization") authorization: String,

View File

@ -16,6 +16,7 @@ import com.nextcloud.talk.conversationcreation.ConversationCreationViewModel
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel
import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel
import com.nextcloud.talk.diagnose.DiagnoseViewModel
import com.nextcloud.talk.invitation.viewmodels.InvitationsViewModel
import com.nextcloud.talk.messagesearch.MessageSearchViewModel
import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel
@ -148,4 +149,9 @@ abstract class ViewModelModule {
@IntoMap
@ViewModelKey(ConversationCreationViewModel::class)
abstract fun conversationCreationViewModel(viewModel: ConversationCreationViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(DiagnoseViewModel::class)
abstract fun diagnoseViewModel(viewModel: DiagnoseViewModel): ViewModel
}

View File

@ -23,10 +23,12 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.ViewModelProvider
import androidx.core.net.toUri
import autodagger.AutoInjector
import com.nextcloud.talk.BuildConfig
@ -35,8 +37,8 @@ import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.components.StandardAppBar
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.BrandingUtils
import com.nextcloud.talk.utils.ClosedInterfaceImpl
@ -56,6 +58,9 @@ class DiagnoseActivity : BaseActivity() {
@Inject
lateinit var arbitraryStorageManager: ArbitraryStorageManager
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject
lateinit var ncApi: NcApi
@ -78,8 +83,13 @@ class DiagnoseActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
val diagnoseViewModel = ViewModelProvider(
this,
viewModelFactory
)[DiagnoseViewModel::class.java]
val colorScheme = viewThemeUtils.getColorScheme(this)
isGooglePlayServicesAvailable = ClosedInterfaceImpl().isGooglePlayServicesAvailable
setContent {
val backgroundColor = colorResource(id = R.color.bg_default)
@ -107,13 +117,22 @@ class DiagnoseActivity : BaseActivity() {
)
},
content = {
val viewState = diagnoseViewModel.notificationViewState.collectAsState().value
Column(
Modifier
.padding(it)
.background(backgroundColor)
.fillMaxSize()
) {
DiagnoseContentComposable(diagnoseDataState)
DiagnoseContentComposable(
diagnoseDataState,
isLoading = diagnoseViewModel.isLoading.value,
showDialog = diagnoseViewModel.showDialog.value,
viewState = viewState,
onTestPushClick = { diagnoseViewModel.fetchTestPushResult() },
onDismissDialog = { diagnoseViewModel.dismissDialog() },
isGooglePlayServicesAvailable = isGooglePlayServicesAvailable
)
}
}
)
@ -126,8 +145,6 @@ class DiagnoseActivity : BaseActivity() {
super.onResume()
supportActionBar?.show()
isGooglePlayServicesAvailable = ClosedInterfaceImpl().isGooglePlayServicesAvailable
diagnoseData.clear()
setupMetaValues()
setupPhoneValues()

View File

@ -7,28 +7,61 @@
package com.nextcloud.talk.diagnose
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
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.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
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.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.nextcloud.talk.R
@Suppress("LongParameterList")
@Composable
fun DiagnoseContentComposable(data: State<List<DiagnoseActivity.DiagnoseElement>>) {
fun DiagnoseContentComposable(
data: State<List<DiagnoseActivity.DiagnoseElement>>,
isLoading: Boolean,
showDialog: Boolean,
viewState: NotificationUiState,
onTestPushClick: () -> Unit,
onDismissDialog: () -> Unit,
isGooglePlayServicesAvailable: Boolean
) {
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxSize()
@ -37,13 +70,17 @@ fun DiagnoseContentComposable(data: State<List<DiagnoseActivity.DiagnoseElement>
) {
data.value.forEach { element ->
when (element) {
is DiagnoseActivity.DiagnoseElement.DiagnoseHeadline -> Text(
modifier = Modifier.padding(vertical = 16.dp),
text = element.headline,
color = MaterialTheme.colorScheme.primary,
fontSize = LocalDensity.current.run { dimensionResource(R.dimen.headline_text_size).toPx().toSp() },
fontWeight = FontWeight.Bold
)
is DiagnoseActivity.DiagnoseElement.DiagnoseHeadline -> {
Text(
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
text = element.headline,
color = MaterialTheme.colorScheme.primary,
fontSize = LocalDensity.current.run {
dimensionResource(R.dimen.headline_text_size).toPx().toSp()
},
fontWeight = FontWeight.Bold
)
}
is DiagnoseActivity.DiagnoseElement.DiagnoseEntry -> {
Text(
@ -59,6 +96,143 @@ fun DiagnoseContentComposable(data: State<List<DiagnoseActivity.DiagnoseElement>
}
}
}
if (isGooglePlayServicesAvailable) {
ShowTestPushButton(onTestPushClick)
}
ShowNotificationData(isLoading, showDialog, context, viewState, onDismissDialog)
}
}
@Composable
fun ShowTestPushButton(onTestPushClick: () -> Unit) {
Button(
modifier = Modifier
.wrapContentSize()
.padding(vertical = 8.dp),
onClick = {
onTestPushClick()
}
) {
Text(
text = stringResource(R.string.nc_test_push_button),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
fontSize = LocalDensity.current.run {
dimensionResource(R.dimen.headline_text_size).toPx().toSp()
},
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
}
@Composable
fun ShowOptions(onDismissDialog: () -> Unit, message: String, context: Context) {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxWidth()
) {
TextButton(onClick = { onDismissDialog() }) {
Text(text = stringResource(R.string.nc_cancel))
}
Spacer(modifier = Modifier.width(8.dp))
TextButton(onClick = {
val clipboard =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Push Message", message)
clipboard.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Toast.makeText(context, R.string.message_copied, Toast.LENGTH_SHORT).show()
}
onDismissDialog()
}) {
Text(text = stringResource(R.string.nc_common_copy))
}
}
}
@Composable
fun LoadingIndicator() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
@Composable
fun ShowNotificationData(
isLoading: Boolean,
showDialog: Boolean,
context: Context,
viewState: NotificationUiState,
onDismissDialog: () -> Unit
) {
val message = getMessage(context, viewState)
if (isLoading) {
LoadingIndicator()
}
if (showDialog) {
Dialog(
onDismissRequest = { onDismissDialog() },
properties = DialogProperties(
dismissOnClickOutside = true,
usePlatformDefaultWidth = false
)
) {
Surface(
shape = MaterialTheme.shapes.medium,
tonalElevation = 8.dp,
modifier = Modifier
.wrapContentSize()
.padding(16.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.nc_test_results),
style = MaterialTheme.typography
.titleMedium
)
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f, fill = false)
.verticalScroll(rememberScrollState())
) {
Column(modifier = Modifier.padding(top = 12.dp)) {
if (viewState is NotificationUiState.Success) {
Text(
text = stringResource(R.string.nc_push_notification_message),
color = colorResource(R.color.colorPrimary)
)
}
Text(
modifier = Modifier.padding(top = 12.dp),
text = message
)
}
}
Spacer(modifier = Modifier.height(16.dp))
ShowOptions(onDismissDialog, message, context)
}
}
}
}
}
fun getMessage(context: Context, viewState: NotificationUiState): String {
return when (viewState) {
is NotificationUiState.Success ->
viewState.testNotification ?: context.getString(R.string.nc_push_notification_fetch_error)
is NotificationUiState.Error ->
context.getString(R.string.nc_push_notification_error, viewState.message)
else ->
context.getString(R.string.nc_common_error_sorry)
}
}
@ -73,5 +247,13 @@ fun DiagnoseContentPreview() {
)
)
}
DiagnoseContentComposable(state)
DiagnoseContentComposable(
state,
false,
true,
NotificationUiState.Success("Test notification successful"),
{},
{},
true
)
}

View File

@ -0,0 +1,69 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.diagnose
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nextcloud.talk.api.NcApiCoroutines
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("TooGenericExceptionCaught")
class DiagnoseViewModel @Inject constructor(
private val ncApiCoroutines: NcApiCoroutines,
private val currentUserProvider: CurrentUserProviderNew
) : ViewModel() {
private val _currentUser = currentUserProvider.currentUser.blockingGet()
val currentUser: User = _currentUser
val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) ?: ""
private val _notificationViewState = MutableStateFlow<NotificationUiState>(NotificationUiState.None)
val notificationViewState: StateFlow<NotificationUiState> = _notificationViewState
private val _isLoading = mutableStateOf(false)
val isLoading = _isLoading
private val _showDialog = mutableStateOf(false)
val showDialog = _showDialog
fun fetchTestPushResult() {
viewModelScope.launch {
try {
_isLoading.value = true
val response = ncApiCoroutines.testPushNotifications(
credentials,
ApiUtils
.getUrlForTestPushNotifications(_currentUser.baseUrl ?: "")
)
val notificationMessage = response.ocs?.data?.message
_notificationViewState.value = NotificationUiState.Success(notificationMessage)
} catch (e: Exception) {
_notificationViewState.value = NotificationUiState.Error(e.message ?: "")
} finally {
_isLoading.value = false
_showDialog.value = true
}
}
}
fun dismissDialog() {
_showDialog.value = false
}
}
sealed class NotificationUiState {
data object None : NotificationUiState()
data class Success(val testNotification: String?) : NotificationUiState()
data class Error(val message: String) : NotificationUiState()
}

View File

@ -0,0 +1,24 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.testNotification
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class TestNotificationData(
@JsonField(name = ["message"])
var message: String
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() :
this("")
}

View File

@ -0,0 +1,26 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.testNotification
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import com.nextcloud.talk.models.json.generic.GenericMeta
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class TestNotificationOCS(
@JsonField(name = ["meta"])
var meta: GenericMeta?,
@JsonField(name = ["data"])
var data: TestNotificationData?
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null)
}

View File

@ -0,0 +1,23 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.testNotification
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class TestNotificationOverall(
@JsonField(name = ["ocs"])
var ocs: TestNotificationOCS?
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null)
}

View File

@ -273,6 +273,10 @@ object ApiUtils {
return getUrlForApi(version, baseUrl) + "/signaling"
}
fun getUrlForTestPushNotifications(baseUrl: String): String {
return "$baseUrl$OCS_API_VERSION/apps/notifications/api/v3/test/self"
}
@JvmStatic
fun getUrlForSignalingBackend(version: Int, baseUrl: String?): String {
return getUrlForSignaling(version, baseUrl) + "/backend"

View File

@ -67,6 +67,8 @@ How to translate with transifex:
<string name="nc_display_name_not_fetched">Display name couldn\'t be fetched, aborting</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s not available (not installed or restricted by admin)</string>
<string name="nc_display_name_not_stored">Could not store display name, aborting</string>
<string name="nc_push_notification_error"> Sorry something went wrong, error is %1$s</string>
<string name="nc_push_notification_fetch_error">Sorry something went wrong, cannot fetch test push message</string>
<string name="nc_search">Search</string>
@ -228,6 +230,11 @@ How to translate with transifex:
<string name="send_email">Send email</string>
<string name="create_issue">Create issue</string>
<string name="nc_diagnose_flavor" translatable="false">Build flavor</string>
<string name="nc_test_push_button">"Test push notifications</string>
<string name="nc_test_results">Test results</string>
<string name="message_copied">Message copied</string>
<string name="nc_push_notification_message">Push notification is sent successfully. You should now receive
a notification on this device with the title \'Testing push notifications\' </string>
<!-- Conversation menu -->
<string name="nc_leave">Leave conversation</string>