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.AddParticipantOverall
import com.nextcloud.talk.models.json.participants.TalkBan import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.models.json.participants.TalkBanOverall 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 com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
@ -238,6 +239,12 @@ interface NcApiCoroutines {
@Url url: String @Url url: String
): UserAbsenceOverall ): UserAbsenceOverall
@POST
suspend fun testPushNotifications(
@Header("Authorization") authorization: String,
@Url url: String
): TestNotificationOverall
@GET @GET
suspend fun getContextOfChatMessage( suspend fun getContextOfChatMessage(
@Header("Authorization") authorization: String, @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.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel
import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel
import com.nextcloud.talk.diagnose.DiagnoseViewModel
import com.nextcloud.talk.invitation.viewmodels.InvitationsViewModel import com.nextcloud.talk.invitation.viewmodels.InvitationsViewModel
import com.nextcloud.talk.messagesearch.MessageSearchViewModel import com.nextcloud.talk.messagesearch.MessageSearchViewModel
import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel
@ -148,4 +149,9 @@ abstract class ViewModelModule {
@IntoMap @IntoMap
@ViewModelKey(ConversationCreationViewModel::class) @ViewModelKey(ConversationCreationViewModel::class)
abstract fun conversationCreationViewModel(viewModel: ConversationCreationViewModel): ViewModel 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.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.ViewModelProvider
import androidx.core.net.toUri import androidx.core.net.toUri
import autodagger.AutoInjector import autodagger.AutoInjector
import com.nextcloud.talk.BuildConfig 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.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.components.StandardAppBar import com.nextcloud.talk.components.StandardAppBar
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.BrandingUtils import com.nextcloud.talk.utils.BrandingUtils
import com.nextcloud.talk.utils.ClosedInterfaceImpl import com.nextcloud.talk.utils.ClosedInterfaceImpl
@ -56,6 +58,9 @@ class DiagnoseActivity : BaseActivity() {
@Inject @Inject
lateinit var arbitraryStorageManager: ArbitraryStorageManager lateinit var arbitraryStorageManager: ArbitraryStorageManager
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject @Inject
lateinit var ncApi: NcApi lateinit var ncApi: NcApi
@ -78,8 +83,13 @@ class DiagnoseActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
val diagnoseViewModel = ViewModelProvider(
this,
viewModelFactory
)[DiagnoseViewModel::class.java]
val colorScheme = viewThemeUtils.getColorScheme(this) val colorScheme = viewThemeUtils.getColorScheme(this)
isGooglePlayServicesAvailable = ClosedInterfaceImpl().isGooglePlayServicesAvailable
setContent { setContent {
val backgroundColor = colorResource(id = R.color.bg_default) val backgroundColor = colorResource(id = R.color.bg_default)
@ -107,13 +117,22 @@ class DiagnoseActivity : BaseActivity() {
) )
}, },
content = { content = {
val viewState = diagnoseViewModel.notificationViewState.collectAsState().value
Column( Column(
Modifier Modifier
.padding(it) .padding(it)
.background(backgroundColor) .background(backgroundColor)
.fillMaxSize() .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() super.onResume()
supportActionBar?.show() supportActionBar?.show()
isGooglePlayServicesAvailable = ClosedInterfaceImpl().isGooglePlayServicesAvailable
diagnoseData.clear() diagnoseData.clear()
setupMetaValues() setupMetaValues()
setupPhoneValues() setupPhoneValues()

View File

@ -7,28 +7,61 @@
package com.nextcloud.talk.diagnose 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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.nextcloud.talk.R import com.nextcloud.talk.R
@Suppress("LongParameterList")
@Composable @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( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -37,13 +70,17 @@ fun DiagnoseContentComposable(data: State<List<DiagnoseActivity.DiagnoseElement>
) { ) {
data.value.forEach { element -> data.value.forEach { element ->
when (element) { when (element) {
is DiagnoseActivity.DiagnoseElement.DiagnoseHeadline -> Text( is DiagnoseActivity.DiagnoseElement.DiagnoseHeadline -> {
modifier = Modifier.padding(vertical = 16.dp), Text(
text = element.headline, modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
color = MaterialTheme.colorScheme.primary, text = element.headline,
fontSize = LocalDensity.current.run { dimensionResource(R.dimen.headline_text_size).toPx().toSp() }, color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold fontSize = LocalDensity.current.run {
) dimensionResource(R.dimen.headline_text_size).toPx().toSp()
},
fontWeight = FontWeight.Bold
)
}
is DiagnoseActivity.DiagnoseElement.DiagnoseEntry -> { is DiagnoseActivity.DiagnoseElement.DiagnoseEntry -> {
Text( 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" return getUrlForApi(version, baseUrl) + "/signaling"
} }
fun getUrlForTestPushNotifications(baseUrl: String): String {
return "$baseUrl$OCS_API_VERSION/apps/notifications/api/v3/test/self"
}
@JvmStatic @JvmStatic
fun getUrlForSignalingBackend(version: Int, baseUrl: String?): String { fun getUrlForSignalingBackend(version: Int, baseUrl: String?): String {
return getUrlForSignaling(version, baseUrl) + "/backend" 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_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_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_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> <string name="nc_search">Search</string>
@ -228,6 +230,11 @@ How to translate with transifex:
<string name="send_email">Send email</string> <string name="send_email">Send email</string>
<string name="create_issue">Create issue</string> <string name="create_issue">Create issue</string>
<string name="nc_diagnose_flavor" translatable="false">Build flavor</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 --> <!-- Conversation menu -->
<string name="nc_leave">Leave conversation</string> <string name="nc_leave">Leave conversation</string>