Skip to content
Open
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 @@ -44,13 +44,12 @@ class ContactsActivity : BaseActivity() {
contactsViewModel.updateIsAddParticipants(isAddParticipants)
contactsViewModel.hideAlreadyAddedParticipants(hideAlreadyAddedParticipants)
if (isAddParticipants) {
contactsViewModel.updateShareTypes(
listOf(
ShareType.Group.shareType,
ShareType.Email.shareType,
ShareType.Circle.shareType
)
)
val onlyLocal = intent.getBooleanExtra(BundleKeys.KEY_ONLY_LOCAL_PARTICIPANTS, false)
val shareTypes = mutableListOf(ShareType.Group.shareType, ShareType.Circle.shareType)
if (!onlyLocal) {
shareTypes.add(ShareType.Email.shareType)
}
contactsViewModel.updateShareTypes(shareTypes)
contactsViewModel.getContactsFromSearchParams()
}
val colorScheme = viewThemeUtils.getColorScheme(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,21 @@ import coil.compose.AsyncImage
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.components.AvatarEditPanel
import com.nextcloud.talk.components.AvatarEditPanelCallbacks
import com.nextcloud.talk.components.AvatarEditPanelState
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.contacts.ContactsActivity
import com.nextcloud.talk.contacts.loadImage
import com.nextcloud.talk.conversationcreation.ui.ConversationPresets
import com.nextcloud.talk.conversationcreation.ui.CreationResultEffect
import com.nextcloud.talk.conversationcreation.ui.ShareCreatedConversation
import com.nextcloud.talk.conversationcreation.ui.openConversation
import com.nextcloud.talk.conversationcreation.viewmodel.ConversationCreationViewModel
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.PickImage
Expand Down Expand Up @@ -409,6 +413,10 @@ fun AddParticipants(
)
intent.putExtra(BundleKeys.KEY_ADD_PARTICIPANTS, true)
intent.putExtra("isAddParticipantsEdit", true)
intent.putExtra(
BundleKeys.KEY_ONLY_LOCAL_PARTICIPANTS,
conversationCreationViewModel.isLockedDown
)
launcher.launch(intent)
},
textAlign = TextAlign.Right
Expand All @@ -418,7 +426,7 @@ fun AddParticipants(
participants.toSet().forEach { participant ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
val imageUri = participant.id?.let {
conversationCreationViewModel.getImageUri(it, true, DisplayUtils.isDarkModeOn(LocalContext.current))
avatarUri(conversationCreationViewModel.currentUser, it, DisplayUtils.isDarkModeOn(context))
}
val errorPlaceholderImage: Int = R.drawable.account_circle_96dp
val loadedImage = loadImage(imageUri, context, errorPlaceholderImage)
Expand All @@ -444,6 +452,10 @@ fun AddParticipants(
.clickable {
val intent = Intent(context, ContactsActivity::class.java)
intent.putExtra(BundleKeys.KEY_ADD_PARTICIPANTS, true)
intent.putExtra(
BundleKeys.KEY_ONLY_LOCAL_PARTICIPANTS,
conversationCreationViewModel.isLockedDown
)
launcher.launch(intent)
},
verticalAlignment = Alignment.CenterVertically
Expand Down Expand Up @@ -472,7 +484,9 @@ fun RoomCreationOptions(conversationCreationViewModel: ConversationCreationViewM
val isOpenForGuestAppUsers = conversationCreationViewModel.isOpenForGuestAppUsers

val isPasswordSet = conversationCreationViewModel.password.collectAsState().value.isNotEmpty()
val isListablePinned = ConversationParameter.LISTABLE in conversationCreationViewModel.pinnedParameters
val isLockedDown = conversationCreationViewModel.isLockedDown
val isListableFixed = isLockedDown ||
ConversationParameter.LISTABLE in conversationCreationViewModel.pinnedParameters

Text(
text = stringResource(id = R.string.nc_new_conversation_visibility),
Expand All @@ -486,6 +500,7 @@ fun RoomCreationOptions(conversationCreationViewModel: ConversationCreationViewM
switch = {
Switch(
checked = isGuestsAllowed,
enabled = !isLockedDown,
onCheckedChange = { conversationCreationViewModel.allowGuests(it) }
)
},
Expand Down Expand Up @@ -514,7 +529,7 @@ fun RoomCreationOptions(conversationCreationViewModel: ConversationCreationViewM
switch = {
Switch(
checked = isConversationAvailableForRegisteredUsers,
enabled = !isListablePinned,
enabled = !isListableFixed,
onCheckedChange = { conversationCreationViewModel.openConversationToRegisteredUsers(it) }
)
},
Expand All @@ -527,7 +542,7 @@ fun RoomCreationOptions(conversationCreationViewModel: ConversationCreationViewM
switch = {
Switch(
checked = isOpenForGuestAppUsers,
enabled = !isListablePinned,
enabled = !isListableFixed,
onCheckedChange = { conversationCreationViewModel.openConversationToGuestAppUsers(it) }
)
},
Expand Down Expand Up @@ -711,6 +726,34 @@ fun ShowPasswordDialog(onDismiss: () -> Unit, conversationCreationViewModel: Con
@Composable
fun CreateConversation(conversationCreationViewModel: ConversationCreationViewModel, context: Context) {
val isCreatingRoom by conversationCreationViewModel.isCreatingRoom.collectAsState()
val creationState by conversationCreationViewModel.creationState.collectAsState()
var createdPublicConversation by rememberSaveable { mutableStateOf<String?>(null) }
var createdWithPassword by rememberSaveable { mutableStateOf(false) }

CreationResultEffect(
creationState = creationState,
context = context,
onPublicConversation = { roomToken, hasPassword ->
createdPublicConversation = roomToken
createdWithPassword = hasPassword
},
onHandled = { conversationCreationViewModel.clearCreationState() }
)

createdPublicConversation?.let { roomToken ->
ShareCreatedConversation(
roomToken = roomToken,
password = conversationCreationViewModel.password.value.takeIf { createdWithPassword },
currentUser = conversationCreationViewModel.currentUser,
context = context,
onDismiss = {
createdPublicConversation = null
conversationCreationViewModel.clearCreationState()
openConversation(context, roomToken)
}
)
}

Box(
modifier = Modifier
.fillMaxWidth()
Expand All @@ -720,14 +763,7 @@ fun CreateConversation(conversationCreationViewModel: ConversationCreationViewMo
Button(
enabled = !isCreatingRoom && !conversationCreationViewModel.isLoadingPresets,
onClick = {
conversationCreationViewModel.createRoomAndAddParticipants { roomToken ->
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken)
val chatIntent = Intent(context, ChatActivity::class.java)
chatIntent.putExtras(bundle)
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
context.startActivity(chatIntent)
}
conversationCreationViewModel.createRoomAndAddParticipants()
}
) {
if (isCreatingRoom) {
Expand Down Expand Up @@ -758,3 +794,6 @@ fun ConversationCreationScreenPreview() {
)
}
}

private fun avatarUri(user: User, avatarId: String, isDarkMode: Boolean): String =
ApiUtils.getUrlForAvatar(user.baseUrl, avatarId, true, darkMode = isDarkMode)
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ class ConversationCreator @Inject constructor(private val repository: Conversati
conversation?.token?.let { token ->
try {
saveAvatar(context, newConversation, token)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.e(TAG, "Failed to set the avatar of the created conversation", e)
}
Expand Down Expand Up @@ -133,7 +135,11 @@ class ConversationCreator @Inject constructor(private val repository: Conversati
} else {
applyAfterCreation(context, newConversation, token)
}
addParticipants(context, newConversation.participants, token)

val failed = addParticipants(context, newConversation.participants, token)
if (failed.isNotEmpty()) {
conversation.invalidParticipants = HashMap(failed)
}

return conversation
}
Expand Down Expand Up @@ -187,38 +193,46 @@ class ConversationCreator @Inject constructor(private val repository: Conversati
}

@Suppress("Detekt.TooGenericExceptionCaught")
private suspend fun addParticipants(context: RequestContext, participants: List<AutocompleteUser>, token: String) {
private suspend fun addParticipants(
context: RequestContext,
participants: List<AutocompleteUser>,
token: String
): Map<String, List<String>> {
val failed = mutableMapOf<String, MutableList<String>>()
participants.forEach { participant ->
val participantId = participant.id ?: return@forEach
val source = participant.source ?: ParticipantSource.USERS
try {
repository.addParticipants(
context.credentials,
ApiUtils.getRetrofitBucketForAddParticipantWithSource(
context.apiVersion,
context.user.baseUrl,
token,
participant.source ?: SOURCE_USERS,
source,
participantId
)
)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.e(TAG, "Failed to add a participant to the created conversation", e)
failed.getOrPut(source) { mutableListOf() }.add(participantId)
}
}
return failed
}

private fun participantsOf(autocompleteUsers: List<AutocompleteUser>): Participants {
val participants = Participants()
autocompleteUsers.forEach { autocompleteUser ->
val id = autocompleteUser.id ?: return@forEach
when (autocompleteUser.source) {
SOURCE_GROUPS -> participants.groups.add(id)
SOURCE_EMAILS -> participants.emails.add(id)
SOURCE_CIRCLES -> participants.teams.add(id)
SOURCE_FEDERATED -> participants.federatedUsers.add(id)
SOURCE_PHONES -> participants.phones.add(id)
ParticipantSource.GROUPS -> participants.groups.add(id)
ParticipantSource.EMAILS -> participants.emails.add(id)
ParticipantSource.CIRCLES -> participants.teams.add(id)
ParticipantSource.FEDERATED -> participants.federatedUsers.add(id)
ParticipantSource.PHONES -> participants.phones.add(id)
else -> participants.users.add(id)
}
}
Expand Down Expand Up @@ -253,11 +267,5 @@ class ConversationCreator @Inject constructor(private val repository: Conversati
companion object {
private val TAG = ConversationCreator::class.simpleName
private const val COLOR_HEX_MASK = 0xFFFFFF
private const val SOURCE_USERS = "users"
private const val SOURCE_GROUPS = "groups"
private const val SOURCE_EMAILS = "emails"
private const val SOURCE_CIRCLES = "circles"
private const val SOURCE_FEDERATED = "federated"
private const val SOURCE_PHONES = "phones"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ object ConversationPresetId {
const val CLASSIFIED = "classified"
const val CHANNEL = "channel"
const val ANNOUNCEMENT = "announcement"

/**
* Types whose conversations are locked down: their visibility can not be changed and only local
* users, groups and teams can be invited.
*/
val lockedDown = setOf(CLASSIFIED)
}

/**
Expand All @@ -68,7 +74,35 @@ fun List<ConversationPresetModel>.parametersFor(
chosenByUser: Map<String, Int> = emptyMap()
): CreateConversationParams =
CreateConversationParams()
.withParameters(parametersOf(ConversationPresetId.DEFAULT))
.withParameters(parametersOf(identifier))
.withParameters(appliedParametersOf(identifier))
.withParameters(chosenByUser)
.withParameters(parametersOf(ConversationPresetId.FORCED))

private fun List<ConversationPresetModel>.appliedParametersOf(identifier: String): Map<String, Int> =
parametersOf(ConversationPresetId.DEFAULT) + parametersOf(identifier)

/**
* The parameters of the given type that are worth showing to the user, leaving out those an
* administrator pinned for every conversation.
*/
fun List<ConversationPresetModel>.parametersToPreviewFor(identifier: String): Map<String, Int> {
val forced = parametersOf(ConversationPresetId.FORCED).keys
return appliedParametersOf(identifier).filterKeys { it !in forced }
}

/**
* Sources an autocomplete result can come from, as the participants API names them.
*/
object ParticipantSource {
const val USERS = "users"
const val GROUPS = "groups"
const val EMAILS = "emails"
const val CIRCLES = "circles"
const val FEDERATED = "federated"
const val PHONES = "phones"

/**
* Sources that stay on this instance. Locked down conversations accept only these.
*/
val local = setOf(USERS, GROUPS, CIRCLES)
}
Loading
Loading