Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f3b8de3
New message description icon change
jbsession Oct 29, 2025
ac4d7c2
Ons lookup failed error
jbsession Oct 29, 2025
65ec34e
unregistered ONS error string
jbsession Oct 29, 2025
edee85b
Modal when clicking help
jbsession Oct 29, 2025
d3b7224
Invalid account id check
jbsession Oct 30, 2025
49d20bb
Updated url dialog commands
jbsession Oct 30, 2025
97ed879
manage admin menu
jbsession Oct 30, 2025
32d2d7f
Removed groupv2 banners
jbsession Oct 30, 2025
91830a2
Leave group option
jbsession Oct 30, 2025
417642a
options
jbsession Nov 3, 2025
06458f3
Initial search with cancel
jbsession Nov 3, 2025
6d81de6
fix search state
jbsession Nov 3, 2025
d93ad02
Animation for focused state:
jbsession Nov 3, 2025
2927aa2
cleanup, Added non-admin members list
jbsession Nov 3, 2025
a6edb1a
Selected items
jbsession Nov 3, 2025
73554cc
Initial bottom footer action
jbsession Nov 3, 2025
fd5d21b
Initial resend invite
jbsession Nov 4, 2025
3b6fb51
Update state when clicking resend
jbsession Nov 4, 2025
73f23ea
Fix search state after resend
jbsession Nov 4, 2025
23ccf9c
New error and toast strings, code cleanup
jbsession Nov 5, 2025
b7386e6
Remove members dialog
jbsession Nov 5, 2025
5351084
cleanup, screen and viewmodel renamed to ManageGroupMembers
jbsession Nov 5, 2025
5873753
clear search state after removing
jbsession Nov 6, 2025
5aebd8d
Fixed some button logic
jbsession Nov 6, 2025
70df9fb
Fixed resend invite quantity screen
jbsession Nov 6, 2025
df3f53e
Empty state
jbsession Nov 6, 2025
59d87e9
Handle chevron search state
jbsession Nov 6, 2025
f8f4fb2
Updated list sorting order
jbsession Nov 6, 2025
1db4656
Cleanup
jbsession Nov 6, 2025
8ed326e
Cleanups
jbsession Nov 6, 2025
fd4391a
Merge branch 'dev' into ses-4753/manage-members
jbsession Nov 6, 2025
360682b
Revert heap size
jbsession Nov 6, 2025
676ad55
Cleanups
jbsession Nov 6, 2025
d315cb8
Updated icon dimens
jbsession Nov 6, 2025
398af1c
Updated nonAdminMember flow
jbsession Nov 7, 2025
65728c9
Fixed unsafe member config
jbsession Nov 7, 2025
682b84c
test
jbsession Nov 7, 2025
850ff03
Revert "test"
jbsession Nov 7, 2025
8a96d6a
test
jbsession Nov 7, 2025
e14871d
Revert "test"
jbsession Nov 7, 2025
a445363
Updated search with x and close button
jbsession Nov 7, 2025
f0e66ae
Added hasMembers flag
jbsession Nov 7, 2025
db69eca
Removed unused composables, flow and function
jbsession Nov 7, 2025
47256e1
Code cleanup for commands, and unused code, renamed item
jbsession Nov 7, 2025
8bdb2d9
Moved options to state, initial uistate
jbsession Nov 7, 2025
f90d1db
Added flows to UIState
jbsession Nov 10, 2025
97ec585
Added footer to the UiState
jbsession Nov 10, 2025
6731b93
updated icon
jbsession Nov 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class GroupInviteException(
val isPromotion: Boolean,
val inviteeAccountIds: List<String>,
val groupName: String,
val isReinvite: Boolean,
underlying: Throwable
) : RuntimeException(underlying) {
init {
Expand All @@ -41,19 +42,26 @@ class GroupInviteException(
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)
val errorString =
if (isPromotion) R.string.adminPromotionFailedDescriptionMultiple else
if (isReinvite) R.string.failedResendInviteMultiple else R.string.groupInviteFailedMultiple
return Phrase.from(context, errorString)
.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)
val errorString = if (isPromotion) R.string.adminPromotionFailedDescriptionTwo else
if (isReinvite) R.string.failedResendInviteTwo else R.string.groupInviteFailedTwo
return Phrase.from(context, errorString)
.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)
val errorString = if (isPromotion) R.string.adminPromotionFailedDescription else
if (isReinvite) R.string.failedResendInvite else R.string.groupInviteFailedUser
return Phrase.from(context, errorString)
.put(NAME_KEY, first)
.put(GROUP_NAME_KEY, groupName)
.format()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.groups.MemberInvite

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

suspend fun reinviteMembers(
group: AccountId,
invites: List<MemberInvite>
)

suspend fun removeMembers(
groupAccountId: AccountId,
removedMembers: List<AccountId>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateM
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Log

class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array<String>) : Job {
class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array<String>, val isReinvite : Boolean) : Job {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might not be safe adding a property directly into an existing job. A job is persisted into db and when it's deserialized, funny thing could happen but i'm not very sure what the behavior here will be. You can try this by inviting someone to the group, using code prior to this PR, then quickly kill the app before the invite completes. Then you install the code from this PR and see whether you'll have trouble reintroduce the job. It could also be fine though, just need some testing


companion object {
const val KEY = "InviteContactJob"
Expand Down Expand Up @@ -122,6 +122,7 @@ class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array<
inviteeAccountIds = failures.map { it.first },
groupName = groupName.orEmpty(),
underlying = firstError,
isReinvite = isReinvite
).format(MessagingModuleConfiguration.shared.context,
MessagingModuleConfiguration.shared.recipientRepository).let {
withContext(Dispatchers.Main) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@ import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.Disappear
import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.*
import org.thoughtcrime.securesms.conversation.v2.settings.notification.NotificationSettingsScreen
import org.thoughtcrime.securesms.conversation.v2.settings.notification.NotificationSettingsViewModel
import org.thoughtcrime.securesms.groups.EditGroupViewModel
import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel
import org.thoughtcrime.securesms.groups.GroupMembersViewModel
import org.thoughtcrime.securesms.groups.SelectContactsViewModel
import org.thoughtcrime.securesms.groups.compose.EditGroupScreen
import org.thoughtcrime.securesms.groups.compose.ManageGroupMembersScreen
import org.thoughtcrime.securesms.groups.compose.GroupMembersScreen
import org.thoughtcrime.securesms.groups.compose.GroupMinimumVersionBanner
import org.thoughtcrime.securesms.groups.compose.InviteContactsScreen
import org.thoughtcrime.securesms.media.MediaOverviewScreen
import org.thoughtcrime.securesms.media.MediaOverviewViewModel
Expand Down Expand Up @@ -182,20 +181,12 @@ fun ConversationSettingsNavHost(
val data: RouteManageMembers = backStackEntry.toRoute()

val viewModel =
hiltViewModel<EditGroupViewModel, EditGroupViewModel.Factory> { factory ->
factory.create(data.groupAddress)
hiltViewModel<ManageGroupMembersViewModel, ManageGroupMembersViewModel.Factory> { factory ->
factory.create(data.groupAddress, navigator)
}

EditGroupScreen(
ManageGroupMembersScreen(
viewModel = viewModel,
navigateToInviteContact = {
navController.navigate(
RouteInviteToGroup(
groupAddress = data.groupAddress,
excludingAccountIDs = viewModel.excludingAccountIDsFromContactSelection.toList()
)
)
},
onBack = dropUnlessResumed {
handleBack()
},
Expand All @@ -219,22 +210,20 @@ fun ConversationSettingsNavHost(
RouteManageMembers(data.groupAddress)
)
}
val editGroupViewModel: EditGroupViewModel = hiltViewModel(parentEntry)
val manageGroupMembersViewModel: ManageGroupMembersViewModel = hiltViewModel(parentEntry)

InviteContactsScreen(
viewModel = viewModel,
onDoneClicked = dropUnlessResumed {
//send invites from the manage group screen
editGroupViewModel.onContactSelected(viewModel.currentSelected)
manageGroupMembersViewModel.onContactSelected(viewModel.currentSelected)

handleBack()
},
onBack = dropUnlessResumed {
handleBack()
},
banner = {
GroupMinimumVersionBanner()
}
banner = {}
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,19 @@ class ConversationSettingsViewModel @AssistedInject constructor(
)
}

private val optionManageAdmins: OptionsItem by lazy{
OptionsItem(
name = context.getString(R.string.manageAdmins),
icon = R.drawable.ic_add_admin_custom,
qaTag = R.string.qa_conversation_settings_manage_members,
onClick = {
(address as? Address.Group)?.let {
navigateTo(ConversationSettingsDestination.RouteManageMembers(it))
}
}
)
}

private val optionLeaveGroup: OptionsItem by lazy{
OptionsItem(
name = context.getString(R.string.groupLeave),
Expand Down Expand Up @@ -569,6 +582,7 @@ class ConversationSettingsViewModel @AssistedInject constructor(
dangerOptions.addAll(
listOf(
optionClearMessages,
optionLeaveGroup,
optionDeleteGroup
)
)
Expand All @@ -577,6 +591,7 @@ class ConversationSettingsViewModel @AssistedInject constructor(
adminOptions.addAll(
listOf(
optionManageMembers,
optionManageAdmins,
optionDisappearingMessage(disappearingSubtitle)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ abstract class BaseGroupMembersViewModel(
::filterContacts
).stateIn(viewModelScope, SharingStarted.Lazily, emptyList())

// Output: List of only NON-ADMINS
val nonAdminMembers: StateFlow<List<GroupMemberState>> = members
.map { list -> list.filter { !it.showAsAdmin } }
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())

val hasNonAdminMembers: StateFlow<Boolean> =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? You already have nonAdminMembers, which you can check for being empty.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this flag since having 0 results when searching/filtering causes the empty list UI to show.

groupInfo
.map { pair -> pair?.second.orEmpty().any { !it.showAsAdmin } }
.stateIn(viewModelScope, SharingStarted.Lazily, false)

fun onSearchQueryChanged(query: String) {
mutableSearchQuery.value = query
}
Expand Down Expand Up @@ -140,7 +150,7 @@ abstract class BaseGroupMembersViewModel(
showProBadge = proStatus.shouldShowProBadge(),
avatarUIData = avatarUtils.getUIDataFromAccountId(memberAccountId.hexString),
clickable = !isMyself,
statusLabel = getMemberLabel(status, context, amIAdmin),
statusLabel = getMemberLabel(status, context, amIAdmin)
)
}

Expand Down Expand Up @@ -172,16 +182,35 @@ abstract class BaseGroupMembersViewModel(
}
}

// Refer to notion doc for the sorting logic
// Refer to manage members/admin PRD for the sorting logic
private fun sortMembers(members: List<GroupMemberState>, currentUserId: AccountId) =
members.sortedWith(
compareBy<GroupMemberState>{ it.accountId != currentUserId } // Current user comes first
.thenBy { !it.showAsAdmin } // Admins come first
.thenComparing(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) // Sort by name (case insensitive)
.thenBy { it.accountId } // Last resort: sort by account ID
compareBy<GroupMemberState> { stateOrder(it.status) }
.thenBy { it.accountId != currentUserId }
.thenComparing(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
.thenBy { it.accountId }
)
}

private fun stateOrder(status: GroupMember.Status?): Int = when (status) {
// 1. Invite failed
GroupMember.Status.INVITE_FAILED -> 0
// 2. Invite not sent
GroupMember.Status.INVITE_NOT_SENT -> 1
// 3. Sending invite
GroupMember.Status.INVITE_SENDING -> 2
// 4. Invite sent
GroupMember.Status.INVITE_SENT -> 3
// 5. Invite status unknown
GroupMember.Status.INVITE_UNKNOWN -> 4
// 6. Pending removal
GroupMember.Status.REMOVED,
GroupMember.Status.REMOVED_UNKNOWN,
GroupMember.Status.REMOVED_INCLUDING_MESSAGES -> 5
// 7. Member (everything else)
else -> 6
}

data class GroupMemberState(
val accountId: AccountId,
val avatarUIData: AvatarUIData,
Expand All @@ -195,7 +224,7 @@ data class GroupMemberState(
val canRemove: Boolean,
val canPromote: Boolean,
val clickable: Boolean,
val statusLabel: String,
val statusLabel: String
) {
val canEdit: Boolean get() = canRemove || canPromote || canResendInvite || canResendPromotion
}
Loading