Merge branch 'master' into remote-desktop

This commit is contained in:
Evgeny Poberezkin
2023-10-29 18:05:03 +00:00
21 changed files with 581 additions and 266 deletions
@@ -1174,9 +1174,9 @@ object ChatController {
}
}
suspend fun apiNewGroup(p: GroupProfile): GroupInfo? {
suspend fun apiNewGroup(incognito: Boolean, groupProfile: GroupProfile): GroupInfo? {
val userId = kotlin.runCatching { currentUserId("apiNewGroup") }.getOrElse { return null }
val r = sendCmd(CC.ApiNewGroup(userId, p))
val r = sendCmd(CC.ApiNewGroup(userId, incognito, groupProfile))
if (r is CR.GroupCreated) return r.groupInfo
Log.e(TAG, "apiNewGroup bad response: ${r.responseType} ${r.details}")
return null
@@ -1951,7 +1951,7 @@ sealed class CC {
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
class ApiNewGroup(val userId: Long, val groupProfile: GroupProfile): CC()
class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC()
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
class ApiJoinGroup(val groupId: Long): CC()
class ApiMemberRole(val groupId: Long, val memberId: Long, val memberRole: GroupMemberRole): CC()
@@ -2074,7 +2074,7 @@ sealed class CC {
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId"
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
is ApiNewGroup -> "/_group $userId ${json.encodeToString(groupProfile)}"
is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}"
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
is ApiJoinGroup -> "/_join #$groupId"
is ApiMemberRole -> "/_member role #$groupId $memberId ${memberRole.memberRole}"
@@ -269,8 +269,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
cancelFile = { fileId ->
withApi { chatModel.controller.cancelFile(user, fileId) }
},
joinGroup = { groupId ->
withApi { chatModel.controller.apiJoinGroup(groupId) }
joinGroup = { groupId, onComplete ->
withApi {
chatModel.controller.apiJoinGroup(groupId)
onComplete.invoke()
}
},
startCall = out@ { media ->
withBGApi {
@@ -386,6 +389,16 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}
}
},
openGroupLink = { groupInfo ->
hideKeyboard(view)
withApi {
val link = chatModel.controller.apiGetGroupLink(groupInfo.groupId)
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) {
GroupLinkView(chatModel, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
}
}
},
markRead = { range, unreadCountAfter ->
chatModel.markChatItemsRead(chat.chatInfo, range, unreadCountAfter)
ntfManager.cancelNotificationsForChat(chat.id)
@@ -431,7 +444,7 @@ fun ChatLayout(
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long, Boolean) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
joinGroup: (Long, () -> Unit) -> Unit,
startCall: (CallMediaType) -> Unit,
endCall: () -> Unit,
acceptCall: (Contact) -> Unit,
@@ -446,6 +459,7 @@ fun ChatLayout(
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
addMembers: (GroupInfo) -> Unit,
openGroupLink: (GroupInfo) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
onSearchValueChanged: (String) -> Unit,
@@ -492,7 +506,7 @@ fun ChatLayout(
}
Scaffold(
topBar = { ChatInfoToolbar(chat, back, info, startCall, endCall, addMembers, changeNtfsState, onSearchValueChanged) },
topBar = { ChatInfoToolbar(chat, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged) },
bottomBar = composeView,
modifier = Modifier.navigationBarsWithImePadding(),
floatingActionButton = { floatingButton.value() },
@@ -523,6 +537,7 @@ fun ChatInfoToolbar(
startCall: (CallMediaType) -> Unit,
endCall: () -> Unit,
addMembers: (GroupInfo) -> Unit,
openGroupLink: (GroupInfo) -> Unit,
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
onSearchValueChanged: (String) -> Unit,
) {
@@ -604,13 +619,24 @@ fun ChatInfoToolbar(
})
}
}
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) {
barButtons.add {
IconButton({
showMenu.value = false
addMembers(chat.chatInfo.groupInfo)
}) {
Icon(painterResource(MR.images.ic_person_add_500), stringResource(MR.strings.icon_descr_add_members), tint = MaterialTheme.colors.primary)
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers) {
if (!chat.chatInfo.incognito) {
barButtons.add {
IconButton({
showMenu.value = false
addMembers(chat.chatInfo.groupInfo)
}) {
Icon(painterResource(MR.images.ic_person_add_500), stringResource(MR.strings.icon_descr_add_members), tint = MaterialTheme.colors.primary)
}
}
} else {
barButtons.add {
IconButton({
showMenu.value = false
openGroupLink(chat.chatInfo.groupInfo)
}) {
Icon(painterResource(MR.images.ic_add_link), stringResource(MR.strings.group_link), tint = MaterialTheme.colors.primary)
}
}
}
}
@@ -720,7 +746,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long, Boolean) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
joinGroup: (Long, () -> Unit) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
openDirectChat: (Long) -> Unit,
@@ -872,7 +898,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
) {
MemberImage(member)
}
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames, developerTools = developerTools)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = { _, _ -> }, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames, developerTools = developerTools)
}
}
} else {
@@ -881,7 +907,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
.then(swipeableModifier)
) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames, developerTools = developerTools)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = { _, _ -> }, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames, developerTools = developerTools)
}
}
}
@@ -891,7 +917,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
.then(swipeableModifier)
) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = { _, _ -> }, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools)
}
}
} else { // direct message
@@ -1323,7 +1349,7 @@ fun PreviewChatLayout() {
deleteMessage = { _, _ -> },
receiveFile = { _, _ -> },
cancelFile = {},
joinGroup = {},
joinGroup = { _, _ -> },
startCall = {},
endCall = {},
acceptCall = { _ -> },
@@ -1338,6 +1364,7 @@ fun PreviewChatLayout() {
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
addMembers = { _ -> },
openGroupLink = {},
markRead = { _, _ -> },
changeNtfsState = { _, _ -> },
onSearchValueChanged = {},
@@ -1393,7 +1420,7 @@ fun PreviewGroupChatLayout() {
deleteMessage = { _, _ -> },
receiveFile = { _, _ -> },
cancelFile = {},
joinGroup = {},
joinGroup = { _, _ -> },
startCall = {},
endCall = {},
acceptCall = { _ -> },
@@ -1408,6 +1435,7 @@ fun PreviewGroupChatLayout() {
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
addMembers = { _ -> },
openGroupLink = {},
markRead = { _, _ -> },
changeNtfsState = { _, _ -> },
onSearchValueChanged = {},
@@ -23,7 +23,15 @@ import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
@Composable
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit) {
fun GroupLinkView(
chatModel: ChatModel,
groupInfo: GroupInfo,
connReqContact: String?,
memberRole: GroupMemberRole?,
onGroupLinkUpdated: ((Pair<String, GroupMemberRole>?) -> Unit)?,
creatingGroup: Boolean = false,
close: (() -> Unit)? = null
) {
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
var creatingLink by rememberSaveable { mutableStateOf(false) }
@@ -34,7 +42,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
onGroupLinkUpdated(link)
onGroupLinkUpdated?.invoke(link)
}
creatingLink = false
}
@@ -58,7 +66,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
onGroupLinkUpdated(link)
onGroupLinkUpdated?.invoke(link)
}
}
}
@@ -73,13 +81,15 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
if (r) {
groupLink = null
onGroupLinkUpdated(null)
onGroupLinkUpdated?.invoke(null)
}
}
},
destructive = true,
)
}
},
creatingGroup = creatingGroup,
close = close
)
if (creatingLink) {
ProgressIndicator()
@@ -94,8 +104,19 @@ fun GroupLinkLayout(
creatingLink: Boolean,
createLink: () -> Unit,
updateLink: () -> Unit,
deleteLink: () -> Unit
deleteLink: () -> Unit,
creatingGroup: Boolean = false,
close: (() -> Unit)? = null
) {
@Composable
fun ContinueButton(close: () -> Unit) {
SimpleButton(
stringResource(MR.strings.continue_to_next_step),
icon = painterResource(MR.images.ic_check),
click = close
)
}
Column(
Modifier
.verticalScroll(rememberScrollState()),
@@ -112,7 +133,16 @@ fun GroupLinkLayout(
verticalArrangement = Arrangement.SpaceEvenly
) {
if (groupLink == null) {
SimpleButton(stringResource(MR.strings.button_create_group_link), icon = painterResource(MR.images.ic_add_link), disabled = creatingLink, click = createLink)
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp)
) {
SimpleButton(stringResource(MR.strings.button_create_group_link), icon = painterResource(MR.images.ic_add_link), disabled = creatingLink, click = createLink)
if (creatingGroup && close != null) {
ContinueButton(close)
}
}
} else {
RoleSelectionRow(groupInfo, groupLinkMemberRole)
var initialLaunch by remember { mutableStateOf(true) }
@@ -134,12 +164,16 @@ fun GroupLinkLayout(
icon = painterResource(MR.images.ic_share),
click = { clipboard.shareText(simplexChatLink(groupLink)) }
)
SimpleButton(
stringResource(MR.strings.delete_link),
icon = painterResource(MR.images.ic_delete),
color = Color.Red,
click = deleteLink
)
if (creatingGroup && close != null) {
ContinueButton(close)
} else {
SimpleButton(
stringResource(MR.strings.delete_link),
icon = painterResource(MR.images.ic_delete),
color = Color.Red,
click = deleteLink
)
}
}
}
}
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.stringResource
@@ -17,6 +18,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
@Composable
fun CIGroupInvitationView(
@@ -24,16 +26,26 @@ fun CIGroupInvitationView(
groupInvitation: CIGroupInvitation,
memberRole: GroupMemberRole,
chatIncognito: Boolean = false,
joinGroup: (Long) -> Unit
joinGroup: (Long, () -> Unit) -> Unit
) {
val sent = ci.chatDir.sent
val action = !sent && groupInvitation.status == CIGroupInvitationStatus.Pending
val inProgress = remember { mutableStateOf(false) }
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(inProgress.value) {
progressByTimeout = if (inProgress.value) {
delay(1000)
inProgress.value
} else {
false
}
}
@Composable
fun groupInfoView() {
val p = groupInvitation.groupProfile
val iconColor =
if (action) if (chatIncognito) Indigo else MaterialTheme.colors.primary
if (action && !inProgress.value) if (chatIncognito) Indigo else MaterialTheme.colors.primary
else if (isInDarkTheme()) FileDark else FileLight
Row(
@@ -70,8 +82,9 @@ fun CIGroupInvitationView(
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Surface(
modifier = if (action) Modifier.clickable(onClick = {
joinGroup(groupInvitation.groupId)
modifier = if (action && !inProgress.value) Modifier.clickable(onClick = {
inProgress.value = true
joinGroup(groupInvitation.groupId) { inProgress.value = false }
}) else Modifier,
shape = RoundedCornerShape(18.dp),
color = if (sent) sentColor else receivedColor,
@@ -83,26 +96,45 @@ fun CIGroupInvitationView(
.padding(start = 8.dp, end = 12.dp),
contentAlignment = Alignment.BottomEnd
) {
Column(
Modifier
.defaultMinSize(minWidth = 220.dp)
.padding(bottom = 4.dp),
Box(
contentAlignment = Alignment.Center
) {
groupInfoView()
Column(Modifier.padding(top = 2.dp, start = 5.dp)) {
Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp))
if (action) {
groupInvitationText()
Text(stringResource(
if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join),
color = if (chatIncognito) Indigo else MaterialTheme.colors.primary)
} else {
Box(Modifier.padding(end = 48.dp)) {
Column(
Modifier
.defaultMinSize(minWidth = 220.dp)
.padding(bottom = 4.dp),
) {
groupInfoView()
Column(Modifier.padding(top = 2.dp, start = 5.dp)) {
Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp))
if (action) {
groupInvitationText()
Text(
stringResource(
if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join
),
color = if (inProgress.value)
MaterialTheme.colors.secondary
else
if (chatIncognito) Indigo else MaterialTheme.colors.primary
)
} else {
Box(Modifier.padding(end = 48.dp)) {
groupInvitationText()
}
}
}
}
if (progressByTimeout) {
CircularProgressIndicator(
Modifier.size(32.dp),
color = if (isInDarkTheme()) FileDark else FileLight,
strokeWidth = 3.dp
)
}
}
Text(
ci.timestampText,
color = MaterialTheme.colors.secondary,
@@ -124,7 +156,7 @@ fun PendingCIGroupInvitationViewPreview() {
ci = ChatItem.getGroupInvitationSample(),
groupInvitation = CIGroupInvitation.getSample(),
memberRole = GroupMemberRole.Admin,
joinGroup = {}
joinGroup = { _, _ -> }
)
}
}
@@ -140,7 +172,7 @@ fun CIGroupInvitationViewAcceptedPreview() {
ci = ChatItem.getGroupInvitationSample(),
groupInvitation = CIGroupInvitation.getSample(status = CIGroupInvitationStatus.Accepted),
memberRole = GroupMemberRole.Admin,
joinGroup = {}
joinGroup = { _, _ -> }
)
}
}
@@ -156,7 +188,7 @@ fun CIGroupInvitationViewLongNamePreview() {
status = CIGroupInvitationStatus.Accepted
),
memberRole = GroupMemberRole.Admin,
joinGroup = {}
joinGroup = { _, _ -> }
)
}
}
@@ -50,7 +50,7 @@ fun ChatItemView(
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long, Boolean) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
joinGroup: (Long, () -> Unit) -> Unit,
acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
@@ -578,7 +578,7 @@ fun PreviewChatItemView() {
deleteMessage = { _, _ -> },
receiveFile = { _, _ -> },
cancelFile = {},
joinGroup = {},
joinGroup = { _, _ -> },
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> },
@@ -609,7 +609,7 @@ fun PreviewChatItemViewDeletedContent() {
deleteMessage = { _, _ -> },
receiveFile = { _, _ -> },
cancelFile = {},
joinGroup = {},
joinGroup = { _, _ -> },
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> },
@@ -12,6 +12,7 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -44,11 +45,22 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
}
val selectedChat = remember(chat.id) { derivedStateOf { chat.id == ChatModel.chatId.value } }
val showChatPreviews = chatModel.showChatPreviews.value
val inProgress = remember { mutableStateOf(false) }
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(inProgress.value) {
progressByTimeout = if (inProgress.value) {
delay(1000)
inProgress.value
} else {
false
}
}
when (chat.chatInfo) {
is ChatInfo.Direct -> {
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
ChatListNavLinkLayout(
chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) },
chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode, inProgress = false, progressByTimeout = false) },
click = { directChatAction(chat.chatInfo, chatModel) },
dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) },
showMenu,
@@ -58,9 +70,9 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
}
is ChatInfo.Group ->
ChatListNavLinkLayout(
chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) },
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) },
chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode, inProgress.value, progressByTimeout) },
click = { if (!inProgress.value) groupChatAction(chat.chatInfo.groupInfo, chatModel, inProgress) },
dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) },
showMenu,
stopped,
selectedChat
@@ -110,9 +122,9 @@ fun directChatAction(chatInfo: ChatInfo, chatModel: ChatModel) {
withBGApi { openChat(chatInfo, chatModel) }
}
fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) {
fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
when (groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(groupInfo, chatModel)
GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(groupInfo, chatModel, inProgress)
GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert()
else -> withBGApi { openChat(ChatInfo.Group(groupInfo), chatModel) }
}
@@ -193,10 +205,19 @@ fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Bo
}
@Composable
fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>, showMarkRead: Boolean) {
fun GroupMenuItems(
chat: Chat,
groupInfo: GroupInfo,
chatModel: ChatModel,
showMenu: MutableState<Boolean>,
inProgress: MutableState<Boolean>,
showMarkRead: Boolean
) {
when (groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> {
JoinGroupAction(chat, groupInfo, chatModel, showMenu)
if (!inProgress.value) {
JoinGroupAction(chat, groupInfo, chatModel, showMenu, inProgress)
}
if (groupInfo.canDelete) {
DeleteGroupAction(chat, groupInfo, chatModel, showMenu)
}
@@ -317,8 +338,20 @@ fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, sh
}
@Composable
fun JoinGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
val joinGroup: () -> Unit = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } }
fun JoinGroupAction(
chat: Chat,
groupInfo: GroupInfo,
chatModel: ChatModel,
showMenu: MutableState<Boolean>,
inProgress: MutableState<Boolean>
) {
val joinGroup: () -> Unit = {
withApi {
inProgress.value = true
chatModel.controller.apiJoinGroup(groupInfo.groupId)
inProgress.value = false
}
}
ItemAction(
if (chat.chatInfo.incognito) stringResource(MR.strings.join_group_incognito_button) else stringResource(MR.strings.join_group_button),
if (chat.chatInfo.incognito) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_login),
@@ -558,12 +591,18 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
)
}
fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel) {
fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.join_group_question),
text = generalGetString(MR.strings.you_are_invited_to_group_join_to_connect_with_group_members),
confirmText = if (groupInfo.membership.memberIncognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { withApi { chatModel.controller.apiJoinGroup(groupInfo.groupId) } },
onConfirm = {
withApi {
inProgress?.value = true
chatModel.controller.apiJoinGroup(groupInfo.groupId)
inProgress?.value = false
}
},
dismissText = generalGetString(MR.strings.delete_verb),
onDismiss = { deleteGroup(groupInfo, chatModel) }
)
@@ -680,7 +719,9 @@ fun PreviewChatListNavLinkDirect() {
null,
null,
stopped = false,
linkMode = SimplexLinkMode.DESCRIPTION
linkMode = SimplexLinkMode.DESCRIPTION,
inProgress = false,
progressByTimeout = false
)
},
click = {},
@@ -721,7 +762,9 @@ fun PreviewChatListNavLinkGroup() {
null,
null,
stopped = false,
linkMode = SimplexLinkMode.DESCRIPTION
linkMode = SimplexLinkMode.DESCRIPTION,
inProgress = false,
progressByTimeout = false
)
},
click = {},
@@ -37,7 +37,9 @@ fun ChatPreviewView(
currentUserProfileDisplayName: String?,
contactNetworkStatus: NetworkStatus?,
stopped: Boolean,
linkMode: SimplexLinkMode
linkMode: SimplexLinkMode,
inProgress: Boolean,
progressByTimeout: Boolean
) {
val cInfo = chat.chatInfo
@@ -135,7 +137,12 @@ fun ChatPreviewView(
}
is ChatInfo.Group ->
when (cInfo.groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> chatPreviewTitleText(if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary)
GroupMemberStatus.MemInvited -> chatPreviewTitleText(
if (inProgress)
MaterialTheme.colors.secondary
else
if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary
)
GroupMemberStatus.MemAccepted -> chatPreviewTitleText(MaterialTheme.colors.secondary)
else -> chatPreviewTitleText()
}
@@ -194,6 +201,17 @@ fun ChatPreviewView(
}
}
@Composable
fun progressView() {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(15.dp),
color = MaterialTheme.colors.secondary,
strokeWidth = 1.5.dp
)
}
@Composable
fun chatStatusImage() {
if (cInfo is ChatInfo.Direct) {
@@ -213,17 +231,17 @@ fun ChatPreviewView(
)
else ->
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(15.dp),
color = MaterialTheme.colors.secondary,
strokeWidth = 1.5.dp
)
progressView()
}
} else {
IncognitoIcon(chat.chatInfo.incognito)
}
} else if (cInfo is ChatInfo.Group) {
if (progressByTimeout) {
progressView()
} else {
IncognitoIcon(chat.chatInfo.incognito)
}
} else {
IncognitoIcon(chat.chatInfo.incognito)
}
@@ -351,6 +369,6 @@ fun unreadCountStr(n: Int): String {
@Composable
fun PreviewChatPreviewView() {
SimpleXTheme {
ChatPreviewView(Chat.sampleData, true, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION)
ChatPreviewView(Chat.sampleData, true, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, progressByTimeout = false)
}
}
@@ -1,5 +1,6 @@
package chat.simplex.common.views.newchat
import SectionTextFooter
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -11,10 +12,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.buildAnnotatedString
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
@@ -22,11 +22,10 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.group.AddGroupMembersView
import chat.simplex.common.views.chatlist.setGroupMembers
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.ReadableText
import chat.simplex.common.views.usersettings.DeleteImageButton
import chat.simplex.common.views.usersettings.EditImageButton
import chat.simplex.common.platform.*
import chat.simplex.common.views.*
import chat.simplex.common.views.chat.group.GroupLinkView
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -35,9 +34,9 @@ import java.net.URI
@Composable
fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
AddGroupLayout(
createGroup = { groupProfile ->
createGroup = { incognito, groupProfile ->
withApi {
val groupInfo = chatModel.controller.apiNewGroup(groupProfile)
val groupInfo = chatModel.controller.apiNewGroup(incognito, groupProfile)
if (groupInfo != null) {
chatModel.addChat(Chat(chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf()))
chatModel.chatItems.clear()
@@ -45,24 +44,36 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
chatModel.chatId.value = groupInfo.id
setGroupMembers(groupInfo, chatModel)
close.invoke()
ModalManager.end.showModalCloseable(true) { close ->
AddGroupMembersView(groupInfo, true, chatModel, close)
if (!groupInfo.incognito) {
ModalManager.end.showModalCloseable(true) { close ->
AddGroupMembersView(groupInfo, creatingGroup = true, chatModel, close)
}
} else {
ModalManager.end.showModalCloseable(true) { close ->
GroupLinkView(chatModel, groupInfo, connReqContact = null, memberRole = null, onGroupLinkUpdated = null, creatingGroup = true, close)
}
}
}
}
},
incognitoPref = chatModel.controller.appPrefs.incognito,
close
)
}
@Composable
fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
fun AddGroupLayout(
createGroup: (Boolean, GroupProfile) -> Unit,
incognitoPref: SharedPreference<Boolean>,
close: () -> Unit
) {
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
val displayName = rememberSaveable { mutableStateOf("") }
val chosenImage = rememberSaveable { mutableStateOf<URI?>(null) }
val profileImage = rememberSaveable { mutableStateOf<String?>(null) }
val focusRequester = remember { FocusRequester() }
val incognito = remember { mutableStateOf(incognitoPref.get()) }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout(
@@ -87,7 +98,6 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(MR.strings.create_secret_group_title))
ReadableText(MR.strings.group_is_decentralized, TextAlign.Center)
Box(
Modifier
.fillMaxWidth()
@@ -118,20 +128,32 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
}
ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester)
Spacer(Modifier.height(8.dp))
val enabled = canCreateProfile(displayName.value)
if (enabled) {
CreateGroupButton(MaterialTheme.colors.primary, Modifier
.clickable {
createGroup(GroupProfile(
displayName = displayName.value.trim(),
fullName = "",
image = profileImage.value
))
}
.padding(8.dp))
} else {
CreateGroupButton(MaterialTheme.colors.secondary, Modifier.padding(8.dp))
}
SettingsActionItem(
painterResource(MR.images.ic_check),
stringResource(MR.strings.create_group_button),
click = {
createGroup(incognito.value, GroupProfile(
displayName = displayName.value.trim(),
fullName = "",
image = profileImage.value
))
},
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary,
disabled = !canCreateProfile(displayName.value)
)
IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } }
SectionTextFooter(
buildAnnotatedString {
append(sharedProfileInfo(chatModel, incognito.value))
append("\n")
append(annotatedStringResource(MR.strings.group_is_decentralized))
}
)
LaunchedEffect(Unit) {
delay(300)
focusRequester.requestFocus()
@@ -142,21 +164,6 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
}
}
@Composable
fun CreateGroupButton(color: Color, modifier: Modifier) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) {
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = color, fontWeight = FontWeight.Bold)
Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = color)
}
}
}
}
fun canCreateProfile(displayName: String): Boolean = displayName.trim().isNotEmpty() && isValidDisplayName(displayName.trim())
@Preview
@@ -164,7 +171,8 @@ fun canCreateProfile(displayName: String): Boolean = displayName.trim().isNotEmp
fun PreviewAddGroupLayout() {
SimpleXTheme {
AddGroupLayout(
createGroup = {},
createGroup = { _, _ -> },
incognitoPref = SharedPreference({ false }, {}),
close = {}
)
}
@@ -3,10 +3,10 @@ package chat.simplex.common.views.newchat
import SectionBottomSpacer
import SectionTextFooter
import androidx.compose.desktop.ui.tooling.preview.Preview
import chat.simplex.common.platform.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource
@@ -14,7 +14,6 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp
import chat.simplex.common.platform.TAG
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.SharedPreference
import chat.simplex.common.ui.theme.*
@@ -23,7 +22,6 @@ import chat.simplex.common.views.usersettings.IncognitoView
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
import java.net.URI
import java.net.URISyntaxException
@Composable
fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
@@ -97,6 +95,8 @@ fun PasteToConnectLayout(
painterResource(MR.images.ic_link),
stringResource(MR.strings.connect_button),
click = { connectViaLink(connectionLink.value) },
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary,
disabled = connectionLink.value.isEmpty() || connectionLink.value.trim().contains(" ")
)
@@ -1294,11 +1294,11 @@
<!-- AddGroupView.kt -->
<string name="create_secret_group_title">Create secret group</string>
<string name="group_is_decentralized">The group is fully decentralized it is visible only to the members.</string>
<string name="group_is_decentralized">Fully decentralized visible only to members.</string>
<string name="group_display_name_field">Enter group name:</string>
<string name="group_full_name_field">Group full name:</string>
<string name="group_main_profile_sent">Your chat profile will be sent to group members</string>
<string name="create_group_button">Create group</string>
<!-- GroupProfileView.kt -->
<string name="group_profile_is_stored_on_members_devices">Group profile is stored on members\' devices, not on the servers.</string>