mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-09 11:46:06 +00:00
android, desktop: offer creating a profile for an invitation
Both profile pickers get an "Add profile" row. It opens the existing CreateProfile form, which gains an optional submit callback, and hands the created profile to the picker's existing selection path - so the create flow and a row tap run the same code. ActiveProfilePicker's row handler is extracted to selectProfile() for that; the body is unchanged apart from indentation. The form opens in ModalManager.fullscreen: on desktop the other placements are panes, where center closes the chat the invitation is in and start and end leave the picker beside it usable while apiChangeConnectionUser invalidates the connection id. The list is refreshed after creating, because keepActiveUser skips the switch that would otherwise do it and nothing else re-lists, and the reassignment is skipped if the form was closed while the profile was being created, when close() would pop the picker instead.
This commit is contained in:
+6
-5
@@ -880,8 +880,8 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, pastTimestamp: Boolean = false, ctrl: ChatCtrl? = null): User? {
|
||||
val r = sendCmd(rh, CC.CreateActiveUser(p, pastTimestamp = pastTimestamp), ctrl)
|
||||
suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, pastTimestamp: Boolean = false, keepActiveUser: Boolean = false, ctrl: ChatCtrl? = null): User? {
|
||||
val r = sendCmd(rh, CC.CreateActiveUser(p, pastTimestamp = pastTimestamp, keepActiveUser = keepActiveUser), ctrl)
|
||||
if (r is API.Result && r.res is CR.ActiveUser) return r.res.user.updateRemoteHostId(rh)
|
||||
val e = (r as? API.Error)?.err
|
||||
if (
|
||||
@@ -3774,7 +3774,7 @@ class SharedPreference<T>(val get: () -> T, set: (T) -> Unit) {
|
||||
sealed class CC {
|
||||
class Console(val cmd: String): CC()
|
||||
class ShowActiveUser: CC()
|
||||
class CreateActiveUser(val profile: Profile?, val pastTimestamp: Boolean): CC()
|
||||
class CreateActiveUser(val profile: Profile?, val pastTimestamp: Boolean, val keepActiveUser: Boolean): CC()
|
||||
class ListUsers: CC()
|
||||
class ApiSetActiveUser(val userId: Long, val viewPwd: String?): CC()
|
||||
class SetAllContactReceipts(val enable: Boolean): CC()
|
||||
@@ -3957,7 +3957,7 @@ sealed class CC {
|
||||
is Console -> cmd
|
||||
is ShowActiveUser -> "/u"
|
||||
is CreateActiveUser -> {
|
||||
val user = NewUser(profile, pastTimestamp = pastTimestamp)
|
||||
val user = NewUser(profile, pastTimestamp = pastTimestamp, keepActiveUser = keepActiveUser)
|
||||
"/_create user ${json.encodeToString(user)}"
|
||||
}
|
||||
is ListUsers -> "/users"
|
||||
@@ -4408,7 +4408,8 @@ fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||
data class NewUser(
|
||||
val profile: Profile?,
|
||||
val pastTimestamp: Boolean,
|
||||
val userChatRelay: Boolean = false
|
||||
val userChatRelay: Boolean = false,
|
||||
val keepActiveUser: Boolean = false
|
||||
)
|
||||
|
||||
sealed class ChatPagination {
|
||||
|
||||
+22
-2
@@ -58,7 +58,7 @@ fun bioFitsLimit(bio: String): Boolean {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
|
||||
fun CreateProfile(chatModel: ChatModel, close: () -> Unit, createProfile: ((String, String, String?) -> Unit)? = null) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val keyboardState by getKeyboardState()
|
||||
@@ -161,7 +161,9 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
if (chatModel.localUserCreated.value == true) {
|
||||
if (createProfile != null) {
|
||||
createProfile(displayName.value, shortDescr.value, profileImage.value)
|
||||
} else if (chatModel.localUserCreated.value == true) {
|
||||
createProfileInProfiles(chatModel, displayName.value, shortDescr.value, profileImage.value, close)
|
||||
} else {
|
||||
createProfileInNoProfileSetup(displayName.value, profileImage.value, close)
|
||||
@@ -353,6 +355,24 @@ private fun CreateFirstProfileDesktop(chatModel: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
fun createProfileForInvitation(rhId: Long?, onCreated: (User) -> Unit) {
|
||||
val modalManager = ModalManager.fullscreen
|
||||
modalManager.showModalCloseable(id = ModalViewId.CONTEXT_USER_PICKER_NEW_PROFILE) { close ->
|
||||
CreateProfile(chatModel, close, createProfile = { displayName, shortDescr, image ->
|
||||
withBGApi {
|
||||
val profile = Profile(displayName.trim(), "", shortDescr.trim().ifEmpty { null }, image = image)
|
||||
val user = controller.apiCreateActiveUser(rhId, profile, keepActiveUser = true) ?: return@withBGApi
|
||||
val users = controller.listUsers(rhId)
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
if (!modalManager.isLastModalOpen(ModalViewId.CONTEXT_USER_PICKER_NEW_PROFILE)) return@withBGApi
|
||||
close()
|
||||
onCreated(user)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun createProfileInNoProfileSetup(displayName: String, image: String? = null, close: () -> Unit) {
|
||||
withBGApi {
|
||||
val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null, image)) ?: return@withBGApi
|
||||
|
||||
+32
@@ -18,7 +18,9 @@ import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.createProfileForInvitation
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.AddProfileOptionImage
|
||||
import chat.simplex.common.views.newchat.IncognitoOptionImage
|
||||
import chat.simplex.common.views.usersettings.IncognitoView
|
||||
import chat.simplex.res.MR
|
||||
@@ -221,6 +223,31 @@ fun ComposeContextProfilePickerView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewProfileOption() {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp)
|
||||
.clickable(onClick = {
|
||||
if (chat.chatInfo.profileChangeProhibited) {
|
||||
showCantChangeProfileAlert()
|
||||
} else {
|
||||
createProfileForInvitation(rhId) { changeProfile(it) }
|
||||
}
|
||||
})
|
||||
.padding(horizontal = DEFAULT_PADDING_HALF, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AddProfileOptionImage()
|
||||
TextIconSpaced(false)
|
||||
Text(
|
||||
stringResource(MR.strings.users_add),
|
||||
color = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfilePicker() {
|
||||
LazyColumnWithScrollBarNoAppBar(
|
||||
@@ -266,6 +293,11 @@ fun ComposeContextProfilePickerView(
|
||||
)
|
||||
ProfilePickerUserOption(user)
|
||||
}
|
||||
|
||||
item {
|
||||
NewProfileOption()
|
||||
if (otherUsers.isEmpty()) Divider(Modifier.padding(horizontal = DEFAULT_PADDING_HALF))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -91,7 +91,8 @@ class ModalData(val keyboardCoversBar: Boolean = true) {
|
||||
|
||||
enum class ModalViewId {
|
||||
SECONDARY_CHAT,
|
||||
CONTEXT_USER_PICKER_INCOGNITO
|
||||
CONTEXT_USER_PICKER_INCOGNITO,
|
||||
CONTEXT_USER_PICKER_NEW_PROFILE
|
||||
}
|
||||
|
||||
class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
|
||||
+75
-44
@@ -39,6 +39,7 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.CIFileViewScope
|
||||
import chat.simplex.common.views.chat.topPaddingToContent
|
||||
import chat.simplex.common.views.createProfileForInvitation
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
@@ -314,6 +315,51 @@ fun ActiveProfilePicker(
|
||||
}
|
||||
}
|
||||
|
||||
fun selectProfile(user: User) {
|
||||
switchingProfile.value = true
|
||||
withApi {
|
||||
try {
|
||||
appPreferences.incognito.set(false)
|
||||
var updatedConn: PendingContactConnection? = null;
|
||||
|
||||
if (contactConnection != null) {
|
||||
updatedConn = controller.apiChangeConnectionUser(rhId, contactConnection.pccConnId, user.userId)
|
||||
if (updatedConn != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateContactConnection(rhId, updatedConn)
|
||||
updateShownConnection(updatedConn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((contactConnection != null && updatedConn != null) || contactConnection == null) {
|
||||
controller.changeActiveUser_(
|
||||
rhId = user.remoteHostId,
|
||||
toUserId = user.userId,
|
||||
viewPwd = if (user.hidden) searchTextOrPassword.value else null
|
||||
)
|
||||
|
||||
if (chatModel.currentUser.value?.userId != user.userId) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(
|
||||
MR.strings.switching_profile_error_title),
|
||||
String.format(generalGetString(MR.strings.switching_profile_error_message), user.chatViewName)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedConn != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateContactConnection(user.remoteHostId, updatedConn)
|
||||
}
|
||||
}
|
||||
|
||||
close()
|
||||
} finally {
|
||||
switchingProfile.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfilePickerUserOption(user: User) {
|
||||
val selected = selectedProfile?.userId == user.userId && !incognito
|
||||
@@ -322,55 +368,23 @@ fun ActiveProfilePicker(
|
||||
title = user.chatViewName,
|
||||
disabled = switchingProfile.value || selected,
|
||||
selected = selected,
|
||||
onSelected = {
|
||||
switchingProfile.value = true
|
||||
withApi {
|
||||
try {
|
||||
appPreferences.incognito.set(false)
|
||||
var updatedConn: PendingContactConnection? = null;
|
||||
|
||||
if (contactConnection != null) {
|
||||
updatedConn = controller.apiChangeConnectionUser(rhId, contactConnection.pccConnId, user.userId)
|
||||
if (updatedConn != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateContactConnection(rhId, updatedConn)
|
||||
updateShownConnection(updatedConn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((contactConnection != null && updatedConn != null) || contactConnection == null) {
|
||||
controller.changeActiveUser_(
|
||||
rhId = user.remoteHostId,
|
||||
toUserId = user.userId,
|
||||
viewPwd = if (user.hidden) searchTextOrPassword.value else null
|
||||
)
|
||||
|
||||
if (chatModel.currentUser.value?.userId != user.userId) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(
|
||||
MR.strings.switching_profile_error_title),
|
||||
String.format(generalGetString(MR.strings.switching_profile_error_message), user.chatViewName)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedConn != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateContactConnection(user.remoteHostId, updatedConn)
|
||||
}
|
||||
}
|
||||
|
||||
close()
|
||||
} finally {
|
||||
switchingProfile.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
onSelected = { selectProfile(user) },
|
||||
image = { ProfileImage(size = 42.dp, image = user.image) },
|
||||
badge = user.profile.localBadge
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewProfileOption() {
|
||||
ProfilePickerOption(
|
||||
title = stringResource(MR.strings.users_add),
|
||||
disabled = switchingProfile.value,
|
||||
selected = false,
|
||||
onSelected = { createProfileForInvitation(rhId) { selectProfile(it) } },
|
||||
image = { AddProfileOptionImage() }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncognitoUserOption() {
|
||||
ProfilePickerOption(
|
||||
@@ -453,6 +467,11 @@ fun ActiveProfilePicker(
|
||||
ProfilePickerUserOption(p)
|
||||
}
|
||||
}
|
||||
if (contactConnection != null) {
|
||||
item {
|
||||
NewProfileOption()
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(Modifier.imePadding().padding(bottom = DEFAULT_BOTTOM_PADDING))
|
||||
}
|
||||
@@ -595,6 +614,18 @@ fun ToggleShortLinkButton(short: MutableState<Boolean>) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddProfileOptionImage() {
|
||||
Box(Modifier.size(42.dp), contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_manage_accounts),
|
||||
contentDescription = null,
|
||||
Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncognitoOptionImage() {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
@@ -114,9 +114,9 @@ enum class OnboardingStage {
|
||||
2. User enters a display name (validated via `chatValidName` JNI) and optional full name.
|
||||
3. On submit, `ChatController.apiCreateActiveUser(rh, profile)` is called:
|
||||
```kotlin
|
||||
suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, pastTimestamp: Boolean = false, ctrl: ChatCtrl? = null): User?
|
||||
suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, pastTimestamp: Boolean = false, keepActiveUser: Boolean = false, ctrl: ChatCtrl? = null): User?
|
||||
```
|
||||
4. The core command `CC.CreateActiveUser(p, pastTimestamp)` creates the user in the database.
|
||||
4. The core command `CC.CreateActiveUser(p, pastTimestamp, keepActiveUser)` creates the user in the database.
|
||||
5. On success, `CR.ActiveUser` returns the new `User` object.
|
||||
6. `ChatModel.currentUser` is set.
|
||||
7. If the chat is not yet running, `startChat(user)` is called:
|
||||
|
||||
@@ -90,7 +90,7 @@ All functions below are `suspend fun` members of `ChatController` ([SimpleXAPI.k
|
||||
| Command | Parameters | Description | Line |
|
||||
|---------|-----------|-------------|------|
|
||||
| `apiGetActiveUser` | `rh: Long?, ctrl: ChatCtrl?` | Fetch the currently active user profile | [L841](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L841) |
|
||||
| `apiCreateActiveUser` | `rh: Long?, p: Profile?, pastTimestamp: Boolean, ctrl: ChatCtrl?` | Create a new user profile and set it as active | [L851](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L851) |
|
||||
| `apiCreateActiveUser` | `rh: Long?, p: Profile?, pastTimestamp: Boolean, keepActiveUser: Boolean, ctrl: ChatCtrl?` | Create a new user profile and set it as active, unless `keepActiveUser` | [L851](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L851) |
|
||||
| `listUsers` | `rh: Long?` | List all user profiles sorted by display name | [L871](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L871) |
|
||||
| `apiSetActiveUser` | `rh: Long?, userId: Long, viewPwd: String?` | Switch the active user to a different profile | [L881](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L881) |
|
||||
| `apiSetAllContactReceipts` | `rh: Long?, enable: Boolean` | Enable/disable delivery receipts for all contacts globally | [L888](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L888) |
|
||||
|
||||
Reference in New Issue
Block a user