[SES-2930] - Show the correct toast message when invitation fails (#900)

* Universal message handling for failing to send invitation

* Comment
pull/1709/head
SessionHero01 3 months ago committed by GitHub
parent ab2bc2ff2b
commit c9c2ccb044
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -230,7 +230,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
for (Map.Entry<String,String> entry : parameters.entrySet()) { for (Map.Entry<String,String> entry : parameters.entrySet()) {
builder.put(entry.getKey(), entry.getValue()); builder.put(entry.getKey(), entry.getValue());
} }
Toast.makeText(getApplicationContext(), builder.format(), toastLength).show(); Toast.makeText(this, builder.format(), toastLength).show();
}
@Override
public void toast(@NonNull CharSequence message, int toastLength) {
Toast.makeText(this, message, toastLength).show();
} }
@Override @Override

@ -61,10 +61,7 @@ abstract class AppBindings {
class ToasterModule { class ToasterModule {
@Provides @Provides
@Singleton @Singleton
fun provideToaster(@ApplicationContext context: Context) = Toaster { stringRes, toastLength, parameters -> fun provideToaster(@ApplicationContext context: Context) = (context as org.thoughtcrime.securesms.ApplicationContext)
val string = context.getString(stringRes, parameters)
Toast.makeText(context, string, toastLength).show()
}
} }
@EntryPoint @EntryPoint

@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.database.StorageProtocol import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupInviteException
import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
@ -78,7 +79,13 @@ class EditGroupViewModel @AssistedInject constructor(
get() = groupInfo.value?.second?.mapTo(hashSetOf()) { it.accountId }.orEmpty() get() = groupInfo.value?.second?.mapTo(hashSetOf()) { it.accountId }.orEmpty()
fun onContactSelected(contacts: Set<AccountId>) { fun onContactSelected(contacts: Set<AccountId>) {
performGroupOperation { performGroupOperation(errorMessage = { err ->
if (err is GroupInviteException) {
err.format(context, storage).toString()
} else {
null
}
}) {
groupManager.inviteMembers( groupManager.inviteMembers(
groupId, groupId,
contacts.toList(), contacts.toList(),
@ -163,7 +170,7 @@ class EditGroupViewModel @AssistedInject constructor(
* This is a helper function that encapsulates the common error handling and progress tracking. * This is a helper function that encapsulates the common error handling and progress tracking.
*/ */
private fun performGroupOperation( private fun performGroupOperation(
genericErrorMessage: (() -> String?)? = null, errorMessage: ((Throwable) -> String?)? = null,
operation: suspend () -> Unit) { operation: suspend () -> Unit) {
viewModelScope.launch { viewModelScope.launch {
mutableInProgress.value = true mutableInProgress.value = true
@ -178,7 +185,7 @@ class EditGroupViewModel @AssistedInject constructor(
try { try {
task.await() task.await()
} catch (e: Exception) { } catch (e: Exception) {
mutableError.value = genericErrorMessage?.invoke() mutableError.value = errorMessage?.invoke(e)
?: context.getString(R.string.errorUnknown) ?: context.getString(R.string.errorUnknown)
} finally { } finally {
mutableInProgress.value = false mutableInProgress.value = false

@ -24,6 +24,7 @@ import network.loki.messenger.libsession_util.util.UserPic
import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol import org.session.libsession.database.StorageProtocol
import org.session.libsession.database.userAuth import org.session.libsession.database.userAuth
import org.session.libsession.messaging.groups.GroupInviteException
import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.groups.GroupScope import org.session.libsession.messaging.groups.GroupScope
import org.session.libsession.messaging.jobs.InviteContactsJob import org.session.libsession.messaging.jobs.InviteContactsJob
@ -279,17 +280,24 @@ class GroupManagerV2Impl @Inject constructor(
// Make sure every request is successful // Make sure every request is successful
response.requireAllRequestsSuccessful("Failed to invite members") response.requireAllRequestsSuccessful("Failed to invite members")
} catch (e: Exception) { } catch (e: Exception) {
// Update every member's status to "invite failed" // Update every member's status to "invite failed" and return group name
configFactory.withMutableGroupConfigs(group) { configs -> val groupName = configFactory.withMutableGroupConfigs(group) { configs ->
for (newMember in newMembers) { for (newMember in newMembers) {
configs.groupMembers.get(newMember.hexString)?.apply { configs.groupMembers.get(newMember.hexString)?.apply {
setInviteFailed() setInviteFailed()
configs.groupMembers.set(this) configs.groupMembers.set(this)
} }
} }
configs.groupInfo.getName().orEmpty()
} }
throw e throw GroupInviteException(
isPromotion = false,
inviteeAccountIds = newMembers.map { it.hexString },
groupName = groupName,
underlying = e
)
} }
// Send the invitation message to the new members // Send the invitation message to the new members
@ -1159,7 +1167,7 @@ class GroupManagerV2Impl @Inject constructor(
sentTimestamp = timestamp sentTimestamp = timestamp
} }
MessageSender.send(message, Destination.ClosedGroup(groupId.hexString), false) MessageSender.send(message, Address.fromSerialized(groupId.hexString))
storage.deleteGroupInfoMessages(groupId, UpdateMessageData.Kind.GroupExpirationUpdated::class.java) storage.deleteGroupInfoMessages(groupId, UpdateMessageData.Kind.GroupExpirationUpdated::class.java)
storage.insertGroupInfoChange(message, groupId) storage.insertGroupInfoChange(message, groupId)

@ -0,0 +1,61 @@
package org.session.libsession.messaging.groups
import android.content.Context
import com.squareup.phrase.Phrase
import org.session.libsession.R
import org.session.libsession.database.StorageProtocol
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.OTHER_NAME_KEY
import org.session.libsession.utilities.truncateIdForDisplay
/**
* Exception that occurs during a group invite.
*
* @param isPromotion Whether the invite was a promotion.
* @param inviteeAccountIds The account IDs of the invitees that failed.
* @param groupName The name of the group.
* @param underlying The underlying exception.
*/
class GroupInviteException(
val isPromotion: Boolean,
val inviteeAccountIds: List<String>,
val groupName: String,
underlying: Throwable
) : RuntimeException(underlying) {
init {
check(inviteeAccountIds.isNotEmpty()) {
"Can not fail a group invite if there are no invitees"
}
}
fun format(context: Context, storage: StorageProtocol): CharSequence {
val getInviteeName = { accountId: String ->
storage.getContactWithAccountID(accountId)?.name ?: truncateIdForDisplay(accountId)
}
val first = inviteeAccountIds.first().let(getInviteeName)
val second = inviteeAccountIds.getOrNull(1)?.let(getInviteeName)
val third = inviteeAccountIds.getOrNull(2)?.let(getInviteeName)
if (second != null && third != null) {
return Phrase.from(context, if (isPromotion) R.string.adminPromotionFailedDescriptionMultiple else R.string.groupInviteFailedMultiple)
.put(NAME_KEY, first)
.put(COUNT_KEY, inviteeAccountIds.size - 1)
.put(GROUP_NAME_KEY, groupName)
.format()
} else if (second != null) {
return Phrase.from(context, if (isPromotion) R.string.adminPromotionFailedDescriptionTwo else R.string.groupInviteFailedTwo)
.put(NAME_KEY, first)
.put(OTHER_NAME_KEY, second)
.put(GROUP_NAME_KEY, groupName)
.format()
} else {
return Phrase.from(context, if (isPromotion) R.string.adminPromotionFailedDescription else R.string.groupInviteFailedUser)
.put(NAME_KEY, first)
.put(GROUP_NAME_KEY, groupName)
.format()
}
}
}

@ -9,6 +9,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.session.libsession.R import org.session.libsession.R
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.groups.GroupInviteException
import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.GroupUpdated import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
@ -111,51 +112,15 @@ class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array<
// show the failure toast // show the failure toast
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val toaster = MessagingModuleConfiguration.shared.toaster val toaster = MessagingModuleConfiguration.shared.toaster
when (failures.size) {
1 -> { GroupInviteException(
val (memberId, _) = failures.first() isPromotion = false,
val firstString = storage.getContactWithAccountID(memberId)?.name inviteeAccountIds = failures.map { it.first },
?: truncateIdForDisplay(memberId) groupName = groupName.orEmpty(),
withContext(Dispatchers.Main) { underlying = failures.first().second.exceptionOrNull()!!,
toaster.toast(R.string.groupInviteFailedUser, Toast.LENGTH_LONG, ).format(MessagingModuleConfiguration.shared.context, storage).let {
mapOf( withContext(Dispatchers.Main) {
NAME_KEY to firstString, toaster.toast(it, Toast.LENGTH_LONG)
GROUP_NAME_KEY to groupName.orEmpty()
)
)
}
}
2 -> {
val (first, second) = failures
val firstString = first.first.let { storage.getContactWithAccountID(it) }?.name
?: truncateIdForDisplay(first.first)
val secondString = second.first.let { storage.getContactWithAccountID(it) }?.name
?: truncateIdForDisplay(second.first)
withContext(Dispatchers.Main) {
toaster.toast(R.string.groupInviteFailedTwo, Toast.LENGTH_LONG,
mapOf(
NAME_KEY to firstString,
OTHER_NAME_KEY to secondString,
GROUP_NAME_KEY to groupName.orEmpty()
)
)
}
}
else -> {
val first = failures.first()
val firstString = first.first.let { storage.getContactWithAccountID(it) }?.name
?: truncateIdForDisplay(first.first)
val remaining = failures.size - 1
withContext(Dispatchers.Main) {
toaster.toast(R.string.groupInviteFailedMultiple, Toast.LENGTH_LONG,
mapOf(
NAME_KEY to firstString,
OTHER_NAME_KEY to remaining.toString(),
GROUP_NAME_KEY to groupName.orEmpty()
)
)
}
} }
} }
} }

@ -2,6 +2,7 @@ package org.session.libsession.utilities
import androidx.annotation.StringRes import androidx.annotation.StringRes
fun interface Toaster { interface Toaster {
fun toast(@StringRes stringRes: Int, toastLength: Int, parameters: Map<String, String>) fun toast(@StringRes stringRes: Int, toastLength: Int, parameters: Map<String, String>)
fun toast(message: CharSequence, toastLength: Int)
} }
Loading…
Cancel
Save