Skip to content

Commit b9fea06

Browse files
authored
SES-4753 : Manage Members pt. 1 (#1665)
* New message description icon change * Ons lookup failed error * unregistered ONS error string * Modal when clicking help * Invalid account id check * Updated url dialog commands * manage admin menu * Removed groupv2 banners * Leave group option * options * Initial search with cancel * fix search state * Animation for focused state: * cleanup, Added non-admin members list * Selected items * Initial bottom footer action * Initial resend invite * Update state when clicking resend * Fix search state after resend * New error and toast strings, code cleanup * Remove members dialog * cleanup, screen and viewmodel renamed to ManageGroupMembers * clear search state after removing * Fixed some button logic * Fixed resend invite quantity screen * Empty state * Handle chevron search state * Updated list sorting order * Cleanup * Cleanups * Revert heap size * Cleanups * Updated icon dimens * Updated nonAdminMember flow * Fixed unsafe member config * test * Revert "test" This reverts commit 682b84c. * test * Revert "test" This reverts commit 8a96d6a. * Updated search with x and close button * Added hasMembers flag * Removed unused composables, flow and function * Code cleanup for commands, and unused code, renamed item * Moved options to state, initial uistate * Added flows to UIState * Added footer to the UiState * updated icon * persist job * Add InviteContactsJob Factory * String ref fix
1 parent a9ce8d7 commit b9fea06

File tree

17 files changed

+1397
-828
lines changed

17 files changed

+1397
-828
lines changed

app/src/main/java/org/session/libsession/messaging/groups/GroupInviteException.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class GroupInviteException(
2323
val isPromotion: Boolean,
2424
val inviteeAccountIds: List<String>,
2525
val groupName: String,
26+
val isReinvite: Boolean,
2627
underlying: Throwable
2728
) : RuntimeException(underlying) {
2829
init {
@@ -41,19 +42,26 @@ class GroupInviteException(
4142
val third = inviteeAccountIds.getOrNull(2)?.let(getInviteeName)
4243

4344
if (second != null && third != null) {
44-
return Phrase.from(context, if (isPromotion) R.string.adminPromotionFailedDescriptionMultiple else R.string.groupInviteFailedMultiple)
45+
val errorString =
46+
if (isPromotion) R.string.adminPromotionFailedDescriptionMultiple else
47+
if (isReinvite) R.string.failedResendInviteMultiple else R.string.groupInviteFailedMultiple
48+
return Phrase.from(context, errorString)
4549
.put(NAME_KEY, first)
4650
.put(COUNT_KEY, inviteeAccountIds.size - 1)
4751
.put(GROUP_NAME_KEY, groupName)
4852
.format()
4953
} else if (second != null) {
50-
return Phrase.from(context, if (isPromotion) R.string.adminPromotionFailedDescriptionTwo else R.string.groupInviteFailedTwo)
54+
val errorString = if (isPromotion) R.string.adminPromotionFailedDescriptionTwo else
55+
if (isReinvite) R.string.failedResendInviteTwo else R.string.groupInviteFailedTwo
56+
return Phrase.from(context, errorString)
5157
.put(NAME_KEY, first)
5258
.put(OTHER_NAME_KEY, second)
5359
.put(GROUP_NAME_KEY, groupName)
5460
.format()
5561
} else {
56-
return Phrase.from(context, if (isPromotion) R.string.adminPromotionFailedDescription else R.string.groupInviteFailedUser)
62+
val errorString = if (isPromotion) R.string.adminPromotionFailedDescription else
63+
if (isReinvite) R.string.failedResendInvite else R.string.groupInviteFailedUser
64+
return Phrase.from(context, errorString)
5765
.put(NAME_KEY, first)
5866
.put(GROUP_NAME_KEY, groupName)
5967
.format()

app/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.session.libsession.messaging.messages.control.GroupUpdated
66
import org.session.libsession.utilities.recipients.Recipient
77
import org.session.protos.SessionProtos.GroupUpdateDeleteMemberContentMessage
88
import org.session.libsignal.utilities.AccountId
9+
import org.thoughtcrime.securesms.groups.MemberInvite
910

1011
/**
1112
* Business logic handling group v2 operations like inviting members,
@@ -25,6 +26,11 @@ interface GroupManagerV2 {
2526
isReinvite: Boolean, // Whether this comes from a re-invite
2627
)
2728

29+
suspend fun reinviteMembers(
30+
group: AccountId,
31+
invites: List<MemberInvite>
32+
)
33+
2834
suspend fun removeMembers(
2935
groupAccountId: AccountId,
3036
removedMembers: List<AccountId>,

app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,17 @@ import org.session.libsignal.utilities.Log
2929
class InviteContactsJob @AssistedInject constructor(
3030
@Assisted val groupSessionId: String,
3131
@Assisted val memberSessionIds: Array<String>,
32+
@Assisted val isReinvite: Boolean,
3233
private val configFactory: ConfigFactoryProtocol,
3334
private val messageSender: MessageSender,
35+
3436
) : Job {
3537

3638
companion object {
3739
const val KEY = "InviteContactJob"
3840
private const val GROUP = "group"
3941
private const val MEMBER = "member"
42+
private const val REINVITE = "reinvite"
4043

4144
}
4245

@@ -130,10 +133,17 @@ class InviteContactsJob @AssistedInject constructor(
130133
inviteeAccountIds = failures.map { it.first },
131134
groupName = groupName.orEmpty(),
132135
underlying = firstError,
133-
).format(MessagingModuleConfiguration.shared.context,
134-
MessagingModuleConfiguration.shared.recipientRepository).let {
136+
isReinvite = isReinvite
137+
).format(
138+
MessagingModuleConfiguration.shared.context,
139+
MessagingModuleConfiguration.shared.recipientRepository
140+
).let {
135141
withContext(Dispatchers.Main) {
136-
Toast.makeText(MessagingModuleConfiguration.shared.context, it, Toast.LENGTH_LONG).show()
142+
Toast.makeText(
143+
MessagingModuleConfiguration.shared.context,
144+
it,
145+
Toast.LENGTH_LONG
146+
).show()
137147
}
138148
}
139149
}
@@ -144,6 +154,7 @@ class InviteContactsJob @AssistedInject constructor(
144154
Data.Builder()
145155
.putString(GROUP, groupSessionId)
146156
.putStringArray(MEMBER, memberSessionIds)
157+
.putBoolean(REINVITE, isReinvite)
147158
.build()
148159

149160
override fun getFactoryKey(): String = KEY
@@ -153,14 +164,17 @@ class InviteContactsJob @AssistedInject constructor(
153164
abstract fun create(
154165
groupSessionId: String,
155166
memberSessionIds: Array<String>,
167+
isReinvite: Boolean
156168
): InviteContactsJob
157169

158170
override fun create(data: Data): InviteContactsJob? {
159171
val groupSessionId = data.getString(GROUP) ?: return null
160172
val memberSessionIds = data.getStringArray(MEMBER) ?: return null
173+
val reinvite = data.getBooleanOrDefault(REINVITE, false)
161174
return create(
162175
groupSessionId = groupSessionId,
163176
memberSessionIds = memberSessionIds,
177+
isReinvite = reinvite
164178
)
165179
}
166180
}

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,11 @@ import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.Disappear
2626
import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.*
2727
import org.thoughtcrime.securesms.conversation.v2.settings.notification.NotificationSettingsScreen
2828
import org.thoughtcrime.securesms.conversation.v2.settings.notification.NotificationSettingsViewModel
29-
import org.thoughtcrime.securesms.groups.EditGroupViewModel
29+
import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel
3030
import org.thoughtcrime.securesms.groups.GroupMembersViewModel
3131
import org.thoughtcrime.securesms.groups.SelectContactsViewModel
32-
import org.thoughtcrime.securesms.groups.compose.EditGroupScreen
32+
import org.thoughtcrime.securesms.groups.compose.ManageGroupMembersScreen
3333
import org.thoughtcrime.securesms.groups.compose.GroupMembersScreen
34-
import org.thoughtcrime.securesms.groups.compose.GroupMinimumVersionBanner
3534
import org.thoughtcrime.securesms.groups.compose.InviteContactsScreen
3635
import org.thoughtcrime.securesms.media.MediaOverviewScreen
3736
import org.thoughtcrime.securesms.media.MediaOverviewViewModel
@@ -184,20 +183,12 @@ fun ConversationSettingsNavHost(
184183
val data: RouteManageMembers = backStackEntry.toRoute()
185184

186185
val viewModel =
187-
hiltViewModel<EditGroupViewModel, EditGroupViewModel.Factory> { factory ->
188-
factory.create(data.groupAddress)
186+
hiltViewModel<ManageGroupMembersViewModel, ManageGroupMembersViewModel.Factory> { factory ->
187+
factory.create(data.groupAddress, navigator)
189188
}
190189

191-
EditGroupScreen(
190+
ManageGroupMembersScreen(
192191
viewModel = viewModel,
193-
navigateToInviteContact = {
194-
navController.navigate(
195-
RouteInviteToGroup(
196-
groupAddress = data.groupAddress,
197-
excludingAccountIDs = viewModel.excludingAccountIDsFromContactSelection.toList()
198-
)
199-
)
200-
},
201192
onBack = dropUnlessResumed {
202193
handleBack()
203194
},
@@ -221,22 +212,20 @@ fun ConversationSettingsNavHost(
221212
RouteManageMembers(data.groupAddress)
222213
)
223214
}
224-
val editGroupViewModel: EditGroupViewModel = hiltViewModel(parentEntry)
215+
val manageGroupMembersViewModel: ManageGroupMembersViewModel = hiltViewModel(parentEntry)
225216

226217
InviteContactsScreen(
227218
viewModel = viewModel,
228219
onDoneClicked = dropUnlessResumed {
229220
//send invites from the manage group screen
230-
editGroupViewModel.onContactSelected(viewModel.currentSelected)
221+
manageGroupMembersViewModel.onContactSelected(viewModel.currentSelected)
231222

232223
handleBack()
233224
},
234225
onBack = dropUnlessResumed {
235226
handleBack()
236227
},
237-
banner = {
238-
GroupMinimumVersionBanner()
239-
}
228+
banner = {}
240229
)
241230
}
242231

app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsViewModel.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,19 @@ class ConversationSettingsViewModel @AssistedInject constructor(
280280
)
281281
}
282282

283+
private val optionManageAdmins: OptionsItem by lazy{
284+
OptionsItem(
285+
name = context.getString(R.string.manageAdmins),
286+
icon = R.drawable.ic_add_admin_custom,
287+
qaTag = R.string.qa_conversation_settings_manage_members,
288+
onClick = {
289+
(address as? Address.Group)?.let {
290+
navigateTo(ConversationSettingsDestination.RouteManageMembers(it))
291+
}
292+
}
293+
)
294+
}
295+
283296
private val optionLeaveGroup: OptionsItem by lazy{
284297
OptionsItem(
285298
name = context.getString(R.string.groupLeave),
@@ -567,6 +580,7 @@ class ConversationSettingsViewModel @AssistedInject constructor(
567580
dangerOptions.addAll(
568581
listOf(
569582
optionClearMessages,
583+
optionLeaveGroup,
570584
optionDeleteGroup
571585
)
572586
)
@@ -575,6 +589,7 @@ class ConversationSettingsViewModel @AssistedInject constructor(
575589
adminOptions.addAll(
576590
listOf(
577591
optionManageMembers,
592+
optionManageAdmins,
578593
optionDisappearingMessage(disappearingSubtitle)
579594
)
580595
)

app/src/main/java/org/thoughtcrime/securesms/groups/BaseGroupMembersViewModel.kt

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@ abstract class BaseGroupMembersViewModel(
8787
::filterContacts
8888
).stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
8989

90+
// Output: List of only NON-ADMINS
91+
val nonAdminMembers: StateFlow<List<GroupMemberState>> = members
92+
.map { list -> list.filter { !it.showAsAdmin } }
93+
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
94+
95+
val hasNonAdminMembers: StateFlow<Boolean> =
96+
groupInfo
97+
.map { pair -> pair?.second.orEmpty().any { !it.showAsAdmin } }
98+
.stateIn(viewModelScope, SharingStarted.Lazily, false)
99+
90100
fun onSearchQueryChanged(query: String) {
91101
mutableSearchQuery.value = query
92102
}
@@ -138,7 +148,7 @@ abstract class BaseGroupMembersViewModel(
138148
showProBadge = shouldShowProBadge,
139149
avatarUIData = avatarUtils.getUIDataFromAccountId(memberAccountId.hexString),
140150
clickable = !isMyself,
141-
statusLabel = getMemberLabel(status, context, amIAdmin),
151+
statusLabel = getMemberLabel(status, context, amIAdmin)
142152
)
143153
}
144154

@@ -170,16 +180,35 @@ abstract class BaseGroupMembersViewModel(
170180
}
171181
}
172182

173-
// Refer to notion doc for the sorting logic
183+
// Refer to manage members/admin PRD for the sorting logic
174184
private fun sortMembers(members: List<GroupMemberState>, currentUserId: AccountId) =
175185
members.sortedWith(
176-
compareBy<GroupMemberState>{ it.accountId != currentUserId } // Current user comes first
177-
.thenBy { !it.showAsAdmin } // Admins come first
178-
.thenComparing(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) // Sort by name (case insensitive)
179-
.thenBy { it.accountId } // Last resort: sort by account ID
186+
compareBy<GroupMemberState> { stateOrder(it.status) }
187+
.thenBy { it.accountId != currentUserId }
188+
.thenComparing(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
189+
.thenBy { it.accountId }
180190
)
181191
}
182192

193+
private fun stateOrder(status: GroupMember.Status?): Int = when (status) {
194+
// 1. Invite failed
195+
GroupMember.Status.INVITE_FAILED -> 0
196+
// 2. Invite not sent
197+
GroupMember.Status.INVITE_NOT_SENT -> 1
198+
// 3. Sending invite
199+
GroupMember.Status.INVITE_SENDING -> 2
200+
// 4. Invite sent
201+
GroupMember.Status.INVITE_SENT -> 3
202+
// 5. Invite status unknown
203+
GroupMember.Status.INVITE_UNKNOWN -> 4
204+
// 6. Pending removal
205+
GroupMember.Status.REMOVED,
206+
GroupMember.Status.REMOVED_UNKNOWN,
207+
GroupMember.Status.REMOVED_INCLUDING_MESSAGES -> 5
208+
// 7. Member (everything else)
209+
else -> 6
210+
}
211+
183212
data class GroupMemberState(
184213
val accountId: AccountId,
185214
val avatarUIData: AvatarUIData,
@@ -193,7 +222,7 @@ data class GroupMemberState(
193222
val canRemove: Boolean,
194223
val canPromote: Boolean,
195224
val clickable: Boolean,
196-
val statusLabel: String,
225+
val statusLabel: String
197226
) {
198227
val canEdit: Boolean get() = canRemove || canPromote || canResendInvite || canResendPromotion
199228
}

0 commit comments

Comments
 (0)