Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -24,6 +24,7 @@ import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.ConfigUpdateNotification
import org.session.libsession.utilities.getMemberName
import org.session.libsignal.utilities.AccountId
import java.util.EnumSet


abstract class BaseGroupMembersViewModel (
Expand All @@ -32,48 +33,35 @@ abstract class BaseGroupMembersViewModel (
private val storage: StorageProtocol,
private val configFactory: ConfigFactoryProtocol
) : ViewModel() {

// Input: invite/promote member's intermediate states. This is needed because we don't have
// a state that we can map into in the config system. The config system only provides "sent", "failed", etc.
// The intermediate states are needed to show the user that the operation is in progress, and the
// states are limited to the view model (i.e. lost if the user navigates away). This is a trade-off
// between the complexity of the config system and the user experience.
protected val memberPendingState = MutableStateFlow<Map<AccountId, MemberPendingState>>(emptyMap())

// Output: the source-of-truth group information. Other states are derived from this.
protected val groupInfo: StateFlow<Pair<GroupDisplayInfo, List<GroupMemberState>>?> =
combine(
configFactory.configUpdateNotifications
.filter {
it is ConfigUpdateNotification.GroupConfigsUpdated && it.groupId == groupId ||
it is ConfigUpdateNotification.UserConfigsMerged
}
.onStart { emit(ConfigUpdateNotification.GroupConfigsUpdated(groupId)) },
memberPendingState
) { _, pending ->
withContext(Dispatchers.Default) {
val currentUserId = AccountId(checkNotNull(storage.getUserPublicKey()) {
"User public key is null"
})

val displayInfo = storage.getClosedGroupDisplayInfo(groupId.hexString)
?: return@withContext null

val members = storage.getMembers(groupId.hexString)
.filterTo(mutableListOf()) { !it.removed }

val memberState = members.map { member ->
createGroupMember(
member = member,
myAccountId = currentUserId,
amIAdmin = displayInfo.isUserAdmin,
pendingState = pending[member.accountId]
)
}

displayInfo to sortMembers(memberState, currentUserId)
configFactory.configUpdateNotifications
.filter {
it is ConfigUpdateNotification.GroupConfigsUpdated && it.groupId == groupId ||
it is ConfigUpdateNotification.UserConfigsMerged
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
.onStart { emit(ConfigUpdateNotification.GroupConfigsUpdated(groupId)) }
.map { _ ->
withContext(Dispatchers.Default) {
val currentUserId = AccountId(checkNotNull(storage.getUserPublicKey()) {
"User public key is null"
})

val displayInfo = storage.getClosedGroupDisplayInfo(groupId.hexString)
?: return@withContext null

val memberState = storage.getMembers(groupId.hexString)
.map { member ->
createGroupMember(
member = member,
myAccountId = currentUserId,
amIAdmin = displayInfo.isUserAdmin,
)
}

displayInfo to sortMembers(memberState, currentUserId)
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)

// Output: the list of the members and their state in the group.
val members: StateFlow<List<GroupMemberState>> = groupInfo
Expand All @@ -84,54 +72,14 @@ abstract class BaseGroupMembersViewModel (
member: GroupMember,
myAccountId: AccountId,
amIAdmin: Boolean,
pendingState: MemberPendingState?
): GroupMemberState {
var status = GroupMemberStatus.MEMBER
var highlightStatus = false
var name = member.getMemberName(configFactory)
var isMyself = false

when {
member.accountIdString() == myAccountId.hexString -> {
name = context.getString(R.string.you)
isMyself = true
}

member.removed -> {
status = GroupMemberStatus.REMOVED
}

pendingState == MemberPendingState.Inviting -> {
status = GroupMemberStatus.INVITE_SENDING
}
val name = member.getMemberName(configFactory)
val isMyself = member.accountId == myAccountId

pendingState == MemberPendingState.Promoting -> {
status = GroupMemberStatus.PROMOTION_SENDING
}

member.status == GroupMember.Status.PROMOTION_SENT -> {
status = GroupMemberStatus.PROMOTION_SENT
}

member.status == GroupMember.Status.INVITE_SENT -> {
status = GroupMemberStatus.INVITE_SENT
}

member.status == GroupMember.Status.INVITE_FAILED -> {
status = GroupMemberStatus.INVITE_FAILED
highlightStatus = true
}

member.status == GroupMember.Status.REMOVED ||
member.status == GroupMember.Status.REMOVED_INCLUDING_MESSAGES -> {
status = GroupMemberStatus.REMOVAL_PENDING
}

member.status == GroupMember.Status.PROMOTION_FAILED -> {
status = GroupMemberStatus.PROMOTION_FAILED
highlightStatus = true
}
}
val highlightStatus = member.status in EnumSet.of(
GroupMember.Status.INVITE_FAILED,
GroupMember.Status.PROMOTION_FAILED
)

return GroupMemberState(
accountId = member.accountId,
Expand All @@ -145,7 +93,7 @@ abstract class BaseGroupMembersViewModel (
canResendInvite = amIAdmin && member.accountId != myAccountId
&& !member.removed
&& (member.status == GroupMember.Status.INVITE_SENT || member.status == GroupMember.Status.INVITE_FAILED),
status = status,
status = member.status?.takeIf { !isMyself }, // Status is only meant for other members
highlightStatus = highlightStatus,
showAsAdmin = member.isAdminOrBeingPromoted,
clickable = !isMyself
Expand All @@ -156,11 +104,11 @@ abstract class BaseGroupMembersViewModel (
members.sortedWith(
compareBy<GroupMemberState>{
when (it.status) {
GroupMemberStatus.INVITE_FAILED -> 0 // Failed invite comes first
GroupMemberStatus.INVITE_SENDING -> 1 // then "Sending invite"
GroupMemberStatus.INVITE_SENT -> 2 // then "Invite sent"
GroupMemberStatus.PROMOTION_SENDING -> 3 // then "Sending promotion"
GroupMemberStatus.PROMOTION_SENT -> 4 // then "Promotion sent"
GroupMember.Status.INVITE_FAILED -> 0 // Failed invite comes first
GroupMember.Status.INVITE_NOT_SENT -> 1 // then "Sending invite"
GroupMember.Status.INVITE_SENT -> 2 // then "Invite sent"
GroupMember.Status.PROMOTION_NOT_SENT -> 3 // then "Sending promotion"
GroupMember.Status.PROMOTION_SENT -> 4 // then "Promotion sent"
else -> 5
}
}
Expand All @@ -174,17 +122,12 @@ abstract class BaseGroupMembersViewModel (
interface Factory {
fun create(groupId: AccountId): EditGroupViewModel
}

protected enum class MemberPendingState {
Inviting,
Promoting,
}
}

data class GroupMemberState(
val accountId: AccountId,
val name: String,
val status: GroupMemberStatus,
val status: GroupMember.Status?,
val highlightStatus: Boolean,
val showAsAdmin: Boolean,
val canResendInvite: Boolean,
Expand All @@ -196,29 +139,22 @@ data class GroupMemberState(
val canEdit: Boolean get() = canRemove || canPromote || canResendInvite || canResendPromotion
}


enum class GroupMemberStatus{
INVITE_FAILED,
INVITE_SENDING,
INVITE_SENT,
PROMOTION_FAILED,
PROMOTION_SENDING,
PROMOTION_SENT,
REMOVAL_PENDING,
REMOVED,
MEMBER;

// Function to get the label dynamically using the context
fun getLabel(context: Context): String {
return when (this) {
INVITE_FAILED -> context.getString(R.string.groupInviteFailed)
INVITE_SENDING -> context.resources.getQuantityString(R.plurals.groupInviteSending, 1)
INVITE_SENT -> context.getString(R.string.groupInviteSent)
PROMOTION_FAILED -> context.getString(R.string.adminPromotionFailed)
PROMOTION_SENDING -> context.resources.getQuantityString(R.plurals.adminSendingPromotion, 1)
PROMOTION_SENT -> context.getString(R.string.adminPromotionSent)
REMOVAL_PENDING -> context.getString(R.string.groupPendingRemoval)
REMOVED, MEMBER -> ""
}
// Function to get the label dynamically using the context
fun GroupMember.Status.getLabel(context: Context): String {
return when (this) {
GroupMember.Status.INVITE_FAILED -> context.getString(R.string.groupInviteFailed)
GroupMember.Status.INVITE_NOT_SENT -> context.resources.getQuantityString(R.plurals.groupInviteSending, 1)
GroupMember.Status.INVITE_SENT -> context.getString(R.string.groupInviteSent)
GroupMember.Status.PROMOTION_FAILED -> context.getString(R.string.adminPromotionFailed)
GroupMember.Status.PROMOTION_NOT_SENT -> context.resources.getQuantityString(R.plurals.adminSendingPromotion, 1)
GroupMember.Status.PROMOTION_SENT -> context.getString(R.string.adminPromotionSent)
GroupMember.Status.REMOVED,
GroupMember.Status.REMOVED_UNKNOWN,
GroupMember.Status.REMOVED_INCLUDING_MESSAGES -> context.getString(R.string.groupPendingRemoval)

GroupMember.Status.INVITE_UNKNOWN,
GroupMember.Status.INVITE_ACCEPTED,
GroupMember.Status.PROMOTION_UNKNOWN,
GroupMember.Status.PROMOTION_ACCEPTED -> ""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,11 @@ class EditGroupViewModel @AssistedInject constructor(

fun onContactSelected(contacts: Set<AccountId>) {
performGroupOperation {
try {
// Mark the contacts as pending
memberPendingState.update { states ->
states + contacts.associateWith { MemberPendingState.Inviting }
}

groupManager.inviteMembers(
groupId,
contacts.toList(),
shareHistory = false
)
} finally {
// Remove pending state (so the real state will be revealed)
memberPendingState.update { states -> states - contacts }
}
groupManager.inviteMembers(
groupId,
contacts.toList(),
shareHistory = false
)
}
}

Expand All @@ -104,15 +94,7 @@ class EditGroupViewModel @AssistedInject constructor(

fun onPromoteContact(memberSessionId: AccountId) {
performGroupOperation {
try {
memberPendingState.update { states ->
states + (memberSessionId to MemberPendingState.Promoting)
}

groupManager.promoteMember(groupId, listOf(memberSessionId))
} finally {
memberPendingState.update { states -> states - memberSessionId }
}
groupManager.promoteMember(groupId, listOf(memberSessionId))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ class GroupManagerV2Impl @Inject constructor(
member.setSupplement(shareHistory)
}

toSet.setInvited()
configs.groupMembers.set(toSet)
}

Expand Down Expand Up @@ -270,7 +271,7 @@ class GroupManagerV2Impl @Inject constructor(
configFactory.withMutableGroupConfigs(group) { configs ->
for (newMember in newMembers) {
configs.groupMembers.get(newMember.hexString)?.apply {
setInvited(failed = true)
setInviteFailed()
configs.groupMembers.set(this)
}
}
Expand Down Expand Up @@ -478,7 +479,15 @@ class GroupManagerV2Impl @Inject constructor(
members: List<AccountId>
): Unit = withContext(dispatcher + SupervisorJob()) {
val adminKey = requireAdminAccess(group)
val groupName = configFactory.withGroupConfigs(group) { it.groupInfo.getName() }
val groupName = configFactory.withMutableGroupConfigs(group) { configs ->
// Update the group member's promotion status
members.asSequence()
.mapNotNull { configs.groupMembers.get(it.hexString) }
.onEach(GroupMember::setPromoted)
.forEach(configs.groupMembers::set)

configs.groupInfo.getName()
}

// Send out the promote message to the members concurrently
val promoteMessage = GroupUpdated(
Expand Down Expand Up @@ -800,7 +809,7 @@ class GroupManagerV2Impl @Inject constructor(
val member = configs.groupMembers.get(sender.hexString)
if (member != null) {
configs.groupMembers.set(member.apply {
setAccepted()
setInviteAccepted()
})
} else {
Log.e(TAG, "User wasn't in the group membership to add!")
Expand Down
Loading