mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-20 11:36:24 +00:00
android, desktop: multiple messages deletion (#4559)
* android, desktop: multiple messages deletion * icons * icon
This commit is contained in:
committed by
GitHub
parent
e769abf14a
commit
b2b1519aea
+20
-1
@@ -1912,7 +1912,7 @@ data class ChatItem (
|
||||
}
|
||||
}
|
||||
|
||||
fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember>? {
|
||||
fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember?>? {
|
||||
return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) {
|
||||
val m = chatInfo.groupInfo.membership
|
||||
if (m.memberRole >= GroupMemberRole.Admin && m.memberRole >= chatDir.groupMember.memberRole && meta.itemDeleted == null) {
|
||||
@@ -1920,11 +1920,30 @@ data class ChatItem (
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupSnd) {
|
||||
val m = chatInfo.groupInfo.membership
|
||||
if (m.memberRole >= GroupMemberRole.Admin) {
|
||||
chatInfo.groupInfo to null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val showLocalDelete: Boolean
|
||||
get() = when (content) {
|
||||
is CIContent.SndDirectE2EEInfo -> false
|
||||
is CIContent.RcvDirectE2EEInfo -> false
|
||||
is CIContent.SndGroupE2EEInfo -> false
|
||||
is CIContent.RcvGroupE2EEInfo -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
val canBeDeletedForSelf: Boolean
|
||||
get() = (content.msgContent != null && !meta.isLive) || meta.itemDeleted != null || isDeletedContent || mergeCategory != null || showLocalDelete
|
||||
|
||||
val showNotification: Boolean get() =
|
||||
when (content) {
|
||||
is CIContent.SndMsgContent -> false
|
||||
|
||||
+266
-106
@@ -1,5 +1,7 @@
|
||||
package chat.simplex.common.views.chat
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.*
|
||||
@@ -50,13 +52,14 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
val shouldReturn = remember { mutableStateOf(false) }
|
||||
val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } }
|
||||
val showSearch = rememberSaveable { mutableStateOf(false) }
|
||||
val activeChatInfo = remember { derivedStateOf {
|
||||
val info = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo
|
||||
if (info == null) {
|
||||
shouldReturn.value = true
|
||||
val activeChatInfo = remember {
|
||||
derivedStateOf {
|
||||
val info = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo
|
||||
if (info == null) {
|
||||
shouldReturn.value = true
|
||||
}
|
||||
return@derivedStateOf info ?: ChatInfo.Direct.sampleData
|
||||
}
|
||||
return@derivedStateOf info ?: ChatInfo.Direct.sampleData
|
||||
}
|
||||
}
|
||||
val user = chatModel.currentUser.value
|
||||
if (shouldReturn.value || user == null) {
|
||||
@@ -82,6 +85,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) }
|
||||
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val selectedChatItems = rememberSaveable { mutableStateOf(null as Set<Long>?) }
|
||||
LaunchedEffect(Unit) {
|
||||
// snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value.
|
||||
// With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view
|
||||
@@ -95,8 +99,12 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
if (chatModel.chatId.value != null) {
|
||||
selectedChatItems.value = null
|
||||
}
|
||||
}
|
||||
val view = LocalMultiplatformView()
|
||||
|
||||
val chatRh = remoteHostId.value
|
||||
// We need to have real unreadCount value for displaying it inside top right button
|
||||
// Having activeChat reloaded on every change in it is inefficient (UI lags)
|
||||
@@ -117,26 +125,61 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
unreadCount,
|
||||
composeState,
|
||||
composeView = {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (
|
||||
chatInfo is ChatInfo.Direct
|
||||
&& !chatInfo.contact.sndReady
|
||||
&& chatInfo.contact.active
|
||||
&& !chatInfo.contact.nextSendGrpInv
|
||||
if (selectedChatItems.value == null) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
generalGetString(MR.strings.contact_connection_pending),
|
||||
Modifier.padding(top = 4.dp),
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
if (
|
||||
chatInfo is ChatInfo.Direct
|
||||
&& !chatInfo.contact.sndReady
|
||||
&& chatInfo.contact.active
|
||||
&& !chatInfo.contact.nextSendGrpInv
|
||||
) {
|
||||
Text(
|
||||
generalGetString(MR.strings.contact_connection_pending),
|
||||
Modifier.padding(top = 4.dp),
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
ComposeView(
|
||||
chatModel, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption,
|
||||
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
|
||||
)
|
||||
}
|
||||
ComposeView(
|
||||
chatModel, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption,
|
||||
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
|
||||
} else {
|
||||
SelectedItemsBottomToolbar(
|
||||
chatItems = remember { chatModel.chatItems }.value,
|
||||
selectedChatItems = selectedChatItems,
|
||||
chatInfo = chatInfo,
|
||||
deleteItems = { canDeleteForAll ->
|
||||
val itemIds = selectedChatItems.value
|
||||
if (itemIds != null) {
|
||||
deleteMessagesAlertDialog(
|
||||
itemIds.sorted(),
|
||||
generalGetString(if (itemIds.size == 1) MR.strings.delete_message_mark_deleted_warning else MR.strings.delete_messages_mark_deleted_warning),
|
||||
forAll = canDeleteForAll,
|
||||
deleteMessages = { ids, forAll ->
|
||||
deleteMessages(chatRh, chatInfo, ids, forAll, moderate = false) {
|
||||
selectedChatItems.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
moderateItems = {
|
||||
if (chatInfo is ChatInfo.Group) {
|
||||
val itemIds = selectedChatItems.value
|
||||
if (itemIds != null) {
|
||||
moderateMessagesAlertDialog(itemIds.sorted(), moderateMessageQuestionText(chatInfo.featureEnabled(ChatFeature.FullDelete), itemIds.size), deleteMessages = { ids ->
|
||||
deleteMessages(chatRh, chatInfo, ids, true, moderate = true) {
|
||||
selectedChatItems.value = null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -145,6 +188,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
searchText,
|
||||
useLinkPreviews = useLinkPreviews,
|
||||
linkMode = chatModel.simplexLinkMode.value,
|
||||
selectedChatItems = selectedChatItems,
|
||||
back = {
|
||||
hideKeyboard(view)
|
||||
AudioPlayer.stop()
|
||||
@@ -271,22 +315,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteMessages = { itemIds ->
|
||||
if (itemIds.isNotEmpty()) {
|
||||
withBGApi {
|
||||
val deleted = chatModel.controller.apiDeleteChatItems(
|
||||
chatRh, chatInfo.chatType, chatInfo.apiId, itemIds, CIDeleteMode.cidmInternal
|
||||
)
|
||||
if (deleted != null) {
|
||||
withChats {
|
||||
for (di in deleted) {
|
||||
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteMessages = { itemIds -> deleteMessages(chatRh, chatInfo, itemIds, false, moderate = false) },
|
||||
receiveFile = { fileId ->
|
||||
withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) }
|
||||
},
|
||||
@@ -536,6 +565,7 @@ fun ChatLayout(
|
||||
searchValue: State<String>,
|
||||
useLinkPreviews: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
back: () -> Unit,
|
||||
info: () -> Unit,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
@@ -613,7 +643,13 @@ fun ChatLayout(
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) },
|
||||
topBar = {
|
||||
if (selectedChatItems.value == null) {
|
||||
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
||||
} else {
|
||||
SelectedItemsTopToolbar(selectedChatItems)
|
||||
}
|
||||
},
|
||||
bottomBar = composeView,
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
floatingActionButton = { floatingButton.value() },
|
||||
@@ -636,7 +672,7 @@ fun ChatLayout(
|
||||
) {
|
||||
ChatItemsList(
|
||||
remoteHostId, chatInfo, unreadCount, composeState, searchValue,
|
||||
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
|
||||
@@ -901,6 +937,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
searchValue: State<String>,
|
||||
useLinkPreviews: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
loadPrevMessages: () -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
@@ -1014,86 +1051,117 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
tryOrShowError("${cItem.id}ChatItem", error = {
|
||||
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
|
||||
}) {
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) {
|
||||
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
|
||||
if (chatInfo is ChatInfo.Group) {
|
||||
if (cItem.chatDir is CIDirection.GroupRcv) {
|
||||
val member = cItem.chatDir.groupMember
|
||||
val (prevMember, memCount) =
|
||||
if (range != null) {
|
||||
chatModel.getPrevHiddenMember(member, range)
|
||||
} else {
|
||||
null to 1
|
||||
}
|
||||
if (prevItem == null || showMemberImage(member, prevItem) || prevMember != null) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
if (cItem.content.showMemberName) {
|
||||
val memberNameStyle = SpanStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
|
||||
val memberNameString = if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
|
||||
buildAnnotatedString {
|
||||
withStyle(memberNameStyle.copy(fontWeight = FontWeight.Medium)) { append(member.memberRole.text) }
|
||||
append(" ")
|
||||
withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) }
|
||||
}
|
||||
} else {
|
||||
buildAnnotatedString {
|
||||
withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) }
|
||||
}
|
||||
}
|
||||
Text(
|
||||
memberNameString,
|
||||
Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp),
|
||||
maxLines = 2
|
||||
)
|
||||
val sent = cItem.chatDir.sent
|
||||
Box(Modifier.padding(bottom = 4.dp)) {
|
||||
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
|
||||
val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf
|
||||
val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp)
|
||||
val swipeableOrSelectionModifier = (if (selectionVisible) Modifier else swipeableModifier).graphicsLayer { translationX = selectionOffset.toPx() }
|
||||
if (chatInfo is ChatInfo.Group) {
|
||||
if (cItem.chatDir is CIDirection.GroupRcv) {
|
||||
val member = cItem.chatDir.groupMember
|
||||
val (prevMember, memCount) =
|
||||
if (range != null) {
|
||||
chatModel.getPrevHiddenMember(member, range)
|
||||
} else {
|
||||
null to 1
|
||||
}
|
||||
Row(
|
||||
swipeableModifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
if (prevItem == null || showMemberImage(member, prevItem) || prevMember != null) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
|
||||
MemberImage(member)
|
||||
if (cItem.content.showMemberName) {
|
||||
val memberNameStyle = SpanStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
|
||||
val memberNameString = if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
|
||||
buildAnnotatedString {
|
||||
withStyle(memberNameStyle.copy(fontWeight = FontWeight.Medium)) { append(member.memberRole.text) }
|
||||
append(" ")
|
||||
withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) }
|
||||
}
|
||||
} else {
|
||||
buildAnnotatedString {
|
||||
withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) }
|
||||
}
|
||||
}
|
||||
Text(
|
||||
memberNameString,
|
||||
Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp),
|
||||
maxLines = 2
|
||||
)
|
||||
}
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier, cItem.id, selectedChatItems)
|
||||
}
|
||||
Row(
|
||||
swipeableOrSelectionModifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
|
||||
MemberImage(member)
|
||||
}
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
|
||||
.then(swipeableOrSelectionModifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
}
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
|
||||
.then(swipeableModifier)
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
|
||||
.then(if (selectionVisible) Modifier else swipeableModifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
Box(
|
||||
Modifier.padding(
|
||||
start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp,
|
||||
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
|
||||
).then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
|
||||
.then(swipeableModifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
val sent = cItem.chatDir.sent
|
||||
Box(
|
||||
Modifier.padding(
|
||||
start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp,
|
||||
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
|
||||
).then(swipeableModifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
if (selectionVisible) {
|
||||
Box(Modifier.matchParentSize().clickable {
|
||||
val checked = selectedChatItems.value?.contains(cItem.id) == true
|
||||
selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1418,6 +1486,96 @@ private fun bottomEndFloatingButton(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectedChatItem(
|
||||
modifier: Modifier,
|
||||
ciId: Long,
|
||||
selectedChatItems: State<Set<Long>?>,
|
||||
) {
|
||||
val checked = remember { derivedStateOf { selectedChatItems.value?.contains(ciId) == true } }
|
||||
Icon(
|
||||
painterResource(if (checked.value) MR.images.ic_check_circle_filled else MR.images.ic_radio_button_unchecked),
|
||||
null,
|
||||
modifier.size(22.dp * fontSizeMultiplier),
|
||||
tint = if (checked.value) {
|
||||
MaterialTheme.colors.primary
|
||||
} else if (isInDarkTheme()) {
|
||||
// .tertiaryLabel instead of .secondary
|
||||
Color(red = 235f / 255f, 235f / 255f, 245f / 255f, 76f / 255f)
|
||||
} else {
|
||||
// .tertiaryLabel instead of .secondary
|
||||
Color(red = 60f / 255f, 60f / 255f, 67f / 255f, 76f / 255f)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun selectUnselectChatItem(select: Boolean, ci: ChatItem, revealed: State<Boolean>, selectedChatItems: MutableState<Set<Long>?>) {
|
||||
val itemIds = mutableSetOf<Long>()
|
||||
if (!revealed.value) {
|
||||
val currIndex = chatModel.getChatItemIndexOrNull(ci)
|
||||
val ciCategory = ci.mergeCategory
|
||||
if (currIndex != null && ciCategory != null) {
|
||||
val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory)
|
||||
val range = chatViewItemsRange(currIndex, prevHidden)
|
||||
if (range != null) {
|
||||
val reversedChatItems = chatModel.chatItems.asReversed()
|
||||
for (i in range) {
|
||||
itemIds.add(reversedChatItems[i].id)
|
||||
}
|
||||
} else {
|
||||
itemIds.add(ci.id)
|
||||
}
|
||||
} else {
|
||||
itemIds.add(ci.id)
|
||||
}
|
||||
} else {
|
||||
itemIds.add(ci.id)
|
||||
}
|
||||
if (select) {
|
||||
val sel = selectedChatItems.value ?: setOf()
|
||||
selectedChatItems.value = sel.union(itemIds)
|
||||
} else {
|
||||
val sel = (selectedChatItems.value ?: setOf()).toMutableSet()
|
||||
sel.removeAll(itemIds)
|
||||
selectedChatItems.value = sel
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long>, forAll: Boolean, moderate: Boolean, onSuccess: () -> Unit = {}) {
|
||||
if (itemIds.isNotEmpty()) {
|
||||
withBGApi {
|
||||
val deleted = if (chatInfo is ChatInfo.Group && forAll && moderate) {
|
||||
chatModel.controller.apiDeleteMemberChatItems(
|
||||
chatRh,
|
||||
groupId = chatInfo.groupInfo.groupId,
|
||||
itemIds = itemIds
|
||||
)
|
||||
} else {
|
||||
chatModel.controller.apiDeleteChatItems(
|
||||
chatRh,
|
||||
type = chatInfo.chatType,
|
||||
id = chatInfo.apiId,
|
||||
itemIds = itemIds,
|
||||
mode = if (forAll) CIDeleteMode.cidmBroadcast else CIDeleteMode.cidmInternal
|
||||
)
|
||||
}
|
||||
if (deleted != null) {
|
||||
withChats {
|
||||
for (di in deleted) {
|
||||
val toChatItem = di.toChatItem?.chatItem
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markUnreadChatAsRead(chatId: String) {
|
||||
val chat = chatModel.chats.value.firstOrNull { it.id == chatId }
|
||||
if (chat?.chatStats?.unreadChat != true) return
|
||||
@@ -1595,6 +1753,7 @@ fun PreviewChatLayout() {
|
||||
searchValue,
|
||||
useLinkPreviews = true,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
selectedChatItems = remember { mutableStateOf(setOf()) },
|
||||
back = {},
|
||||
info = {},
|
||||
showMemberInfo = { _, _ -> },
|
||||
@@ -1666,6 +1825,7 @@ fun PreviewGroupChatLayout() {
|
||||
searchValue,
|
||||
useLinkPreviews = true,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
selectedChatItems = remember { mutableStateOf(setOf()) },
|
||||
back = {},
|
||||
info = {},
|
||||
showMemberInfo = { _, _ -> },
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package chat.simplex.common.views.chat
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.BackHandler
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
|
||||
@Composable
|
||||
fun SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
|
||||
val onBackClicked = { selectedChatItems.value = null }
|
||||
BackHandler(onBack = onBackClicked)
|
||||
val count = selectedChatItems.value?.size ?: 0
|
||||
DefaultTopAppBar(
|
||||
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
|
||||
title = {
|
||||
Text(
|
||||
if (count == 0) {
|
||||
stringResource(MR.strings.selected_chat_items_nothing_selected)
|
||||
} else {
|
||||
stringResource(MR.strings.selected_chat_items_selected_n).format(count)
|
||||
},
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
onTitleClick = null,
|
||||
showSearch = false,
|
||||
onSearchValueChanged = {},
|
||||
)
|
||||
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectedItemsBottomToolbar(
|
||||
chatInfo: ChatInfo,
|
||||
chatItems: List<ChatItem>,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible
|
||||
moderateItems: () -> Unit,
|
||||
// shareItems: () -> Unit,
|
||||
) {
|
||||
val deleteEnabled = remember { mutableStateOf(false) }
|
||||
val deleteForEveryoneEnabled = remember { mutableStateOf(false) }
|
||||
val canModerate = remember { mutableStateOf(false) }
|
||||
val moderateEnabled = remember { mutableStateOf(false) }
|
||||
val allButtonsDisabled = remember { mutableStateOf(false) }
|
||||
Box {
|
||||
// It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty
|
||||
ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {})
|
||||
Row(Modifier.matchParentSize().background(MaterialTheme.colors.background), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton({ deleteItems(deleteForEveryoneEnabled.value) }, enabled = deleteEnabled.value && !allButtonsDisabled.value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_delete),
|
||||
null,
|
||||
Modifier.size(24.dp),
|
||||
tint = if (!deleteEnabled.value || allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
|
||||
IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !allButtonsDisabled.value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_flag),
|
||||
null,
|
||||
Modifier.size(24.dp),
|
||||
tint = if (!moderateEnabled.value || allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
|
||||
IconButton({ /*shareItems()*/ }, Modifier.alpha(0f), enabled = false/*!allButtonsDisabled.value*/) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_share),
|
||||
null,
|
||||
Modifier.size(24.dp),
|
||||
tint = if (allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) {
|
||||
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, allButtonsDisabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun recheckItems(chatInfo: ChatInfo,
|
||||
chatItems: List<ChatItem>,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
deleteEnabled: MutableState<Boolean>,
|
||||
deleteForEveryoneEnabled: MutableState<Boolean>,
|
||||
canModerate: MutableState<Boolean>,
|
||||
moderateEnabled: MutableState<Boolean>,
|
||||
allButtonsDisabled: MutableState<Boolean>
|
||||
) {
|
||||
val count = selectedChatItems.value?.size ?: 0
|
||||
allButtonsDisabled.value = count == 0 || count > 20
|
||||
canModerate.value = possibleToModerate(chatInfo)
|
||||
val selected = selectedChatItems.value ?: return
|
||||
var rDeleteEnabled = true
|
||||
var rDeleteForEveryoneEnabled = true
|
||||
var rModerateEnabled = true
|
||||
var rOnlyOwnGroupItems = true
|
||||
val rSelectedChatItems = mutableSetOf<Long>()
|
||||
for (ci in chatItems) {
|
||||
if (selected.contains(ci.id)) {
|
||||
rDeleteEnabled = rDeleteEnabled && ci.canBeDeletedForSelf
|
||||
rDeleteForEveryoneEnabled = rDeleteForEveryoneEnabled && ci.meta.deletable && !ci.localNote
|
||||
rOnlyOwnGroupItems = rOnlyOwnGroupItems && ci.chatDir is CIDirection.GroupSnd
|
||||
rModerateEnabled = rModerateEnabled && !rOnlyOwnGroupItems && ci.content.msgContent != null && ci.memberToModerate(chatInfo) != null
|
||||
rSelectedChatItems.add(ci.id) // we are collecting new selected items here to account for any changes in chat items list
|
||||
}
|
||||
}
|
||||
deleteEnabled.value = rDeleteEnabled
|
||||
deleteForEveryoneEnabled.value = rDeleteForEveryoneEnabled
|
||||
moderateEnabled.value = rModerateEnabled
|
||||
selectedChatItems.value = rSelectedChatItems
|
||||
}
|
||||
|
||||
private fun possibleToModerate(chatInfo: ChatInfo): Boolean =
|
||||
chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Admin
|
||||
+93
-16
@@ -50,6 +50,8 @@ fun ChatItemView(
|
||||
linkMode: SimplexLinkMode,
|
||||
revealed: MutableState<Boolean>,
|
||||
range: IntRange?,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
selectChatItem: () -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -81,9 +83,7 @@ fun ChatItemView(
|
||||
val live = composeState.value.liveMessage != null
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = alignment,
|
||||
) {
|
||||
val info = cItem.meta.itemStatus.statusInto
|
||||
@@ -142,14 +142,6 @@ fun ChatItemView(
|
||||
}
|
||||
}
|
||||
|
||||
fun moderateMessageQuestionText(): String {
|
||||
return if (fullDeleteAllowed) {
|
||||
generalGetString(MR.strings.moderate_message_will_be_deleted_warning)
|
||||
} else {
|
||||
generalGetString(MR.strings.moderate_message_will_be_marked_warning)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MsgReactionsMenu() {
|
||||
val rs = MsgReaction.values.mapNotNull { r ->
|
||||
@@ -180,6 +172,10 @@ fun ChatItemView(
|
||||
fun DeleteItemMenu() {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,8 +273,12 @@ fun ChatItemView(
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
}
|
||||
val groupInfo = cItem.memberToModerate(cInfo)?.first
|
||||
if (groupInfo != null) {
|
||||
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage)
|
||||
if (groupInfo != null && cItem.chatDir !is CIDirection.GroupSnd) {
|
||||
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(cInfo.featureEnabled(ChatFeature.FullDelete), 1), showMenu, deleteMessage)
|
||||
}
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,12 +293,20 @@ fun ChatItemView(
|
||||
}
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
cItem.isDeletedContent -> {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
cItem.mergeCategory != null && ((range?.count() ?: 0) > 1 || revealed.value) -> {
|
||||
@@ -309,11 +317,19 @@ fun ChatItemView(
|
||||
ExpandItemAction(revealed, showMenu)
|
||||
}
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (selectedChatItems.value == null) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,6 +343,10 @@ fun ChatItemView(
|
||||
}
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,6 +377,10 @@ fun ChatItemView(
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,6 +434,10 @@ fun ChatItemView(
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,7 +635,12 @@ fun DeleteItemAction(
|
||||
for (i in range) {
|
||||
itemIds.add(reversedChatItems[i].id)
|
||||
}
|
||||
deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages)
|
||||
deleteMessagesAlertDialog(
|
||||
itemIds,
|
||||
generalGetString(if (itemIds.size == 1) MR.strings.delete_message_mark_deleted_warning else MR.strings.delete_messages_mark_deleted_warning),
|
||||
forAll = false,
|
||||
deleteMessages = { ids, _ -> deleteMessages(ids) }
|
||||
)
|
||||
} else {
|
||||
deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
|
||||
}
|
||||
@@ -640,6 +673,21 @@ fun ModerateItemAction(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectItemAction(
|
||||
showMenu: MutableState<Boolean>,
|
||||
selectChatItem: () -> Unit,
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.select_verb),
|
||||
painterResource(MR.images.ic_check_circle),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
selectChatItem()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RevealItemAction(revealed: MutableState<Boolean>, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
@@ -784,7 +832,7 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteMessagesAlertDialog(itemIds: List<Long>, questionText: String, deleteMessages: (List<Long>) -> Unit) {
|
||||
fun deleteMessagesAlertDialog(itemIds: List<Long>, questionText: String, forAll: Boolean, deleteMessages: (List<Long>, Boolean) -> Unit) {
|
||||
AlertManager.shared.showAlertDialogButtons(
|
||||
title = generalGetString(MR.strings.delete_messages__question).format(itemIds.size),
|
||||
text = questionText,
|
||||
@@ -796,14 +844,29 @@ fun deleteMessagesAlertDialog(itemIds: List<Long>, questionText: String, deleteM
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
deleteMessages(itemIds)
|
||||
deleteMessages(itemIds, false)
|
||||
AlertManager.shared.hideAlert()
|
||||
}) { Text(stringResource(MR.strings.for_me_only), color = MaterialTheme.colors.error) }
|
||||
|
||||
if (forAll) {
|
||||
TextButton(onClick = {
|
||||
deleteMessages(itemIds, true)
|
||||
AlertManager.shared.hideAlert()
|
||||
}) { Text(stringResource(MR.strings.for_everybody), color = MaterialTheme.colors.error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun moderateMessageQuestionText(fullDeleteAllowed: Boolean, count: Int): String {
|
||||
return if (fullDeleteAllowed) {
|
||||
generalGetString(if (count == 1) MR.strings.moderate_message_will_be_deleted_warning else MR.strings.moderate_messages_will_be_deleted_warning)
|
||||
} else {
|
||||
generalGetString(if (count == 1) MR.strings.moderate_message_will_be_marked_warning else MR.strings.moderate_messages_will_be_marked_warning)
|
||||
}
|
||||
}
|
||||
|
||||
fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.delete_member_message__question),
|
||||
@@ -816,6 +879,16 @@ fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteM
|
||||
)
|
||||
}
|
||||
|
||||
fun moderateMessagesAlertDialog(itemIds: List<Long>, questionText: String, deleteMessages: (List<Long>) -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = if (itemIds.size == 1) generalGetString(MR.strings.delete_member_message__question) else generalGetString(MR.strings.delete_members_messages__question).format(itemIds.size),
|
||||
text = questionText,
|
||||
confirmText = generalGetString(MR.strings.delete_verb),
|
||||
destructive = true,
|
||||
onConfirm = { deleteMessages(itemIds) }
|
||||
)
|
||||
}
|
||||
|
||||
expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager)
|
||||
|
||||
@Preview
|
||||
@@ -832,6 +905,8 @@ fun PreviewChatItemView(
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
revealed = remember { mutableStateOf(false) },
|
||||
range = 0..1,
|
||||
selectedChatItems = remember { mutableStateOf(setOf()) },
|
||||
selectChatItem = {},
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
receiveFile = { _ -> },
|
||||
@@ -869,6 +944,8 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
revealed = remember { mutableStateOf(false) },
|
||||
range = 0..1,
|
||||
selectedChatItems = remember { mutableStateOf(setOf()) },
|
||||
selectChatItem = {},
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
receiveFile = { _ -> },
|
||||
|
||||
+9
@@ -53,6 +53,15 @@ fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationButtonClose(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, height: Dp = 24.dp) {
|
||||
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close), stringResource(MR.strings.back), Modifier.height(height), tint = tintColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShareButton(onButtonClicked: () -> Unit) {
|
||||
IconButton(onButtonClicked) {
|
||||
|
||||
@@ -311,14 +311,19 @@
|
||||
<string name="hide_verb">Hide</string>
|
||||
<string name="allow_verb">Allow</string>
|
||||
<string name="moderate_verb">Moderate</string>
|
||||
<string name="select_verb">Select</string>
|
||||
<string name="expand_verb">Expand</string>
|
||||
<string name="delete_message__question">Delete message?</string>
|
||||
<string name="delete_messages__question">Delete %d messages?</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Message will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_message_mark_deleted_warning">Message will be marked for deletion. The recipient(s) will be able to reveal this message.</string>
|
||||
<string name="delete_messages_mark_deleted_warning">Messages will be marked for deletion. The recipient(s) will be able to reveal these messages.</string>
|
||||
<string name="delete_member_message__question">Delete member message?</string>
|
||||
<string name="delete_members_messages__question">Delete %d messages of members?</string>
|
||||
<string name="moderate_message_will_be_deleted_warning">The message will be deleted for all members.</string>
|
||||
<string name="moderate_messages_will_be_deleted_warning">The messages will be deleted for all members.</string>
|
||||
<string name="moderate_message_will_be_marked_warning">The message will be marked as moderated for all members.</string>
|
||||
<string name="moderate_messages_will_be_marked_warning">The messages will be marked as moderated for all members.</string>
|
||||
<string name="for_me_only">Delete for me</string>
|
||||
<string name="for_everybody">For everyone</string>
|
||||
<string name="stop_file__action">Stop file</string>
|
||||
@@ -368,6 +373,8 @@
|
||||
|
||||
<!-- ChatView.kt -->
|
||||
<string name="no_selected_chat">No selected chat</string>
|
||||
<string name="selected_chat_items_nothing_selected">Nothing selected</string>
|
||||
<string name="selected_chat_items_selected_n">Selected %d</string>
|
||||
|
||||
<!-- ShareListView.kt -->
|
||||
<string name="share_message">Share message…</string>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M421.5-300.5 701-580l-44.5-43.5-235 235-119-119L259-464l162.5 163.5ZM480.06-85q-80.97 0-153.13-31.26-72.15-31.27-125.79-85Q147.5-255 116.25-327.02 85-399.05 85-479.94q0-81.97 31.26-154.13 31.27-72.15 85-125.54Q255-813 327.02-844q72.03-31 152.92-31 81.97 0 154.13 31.13 72.17 31.13 125.55 84.5Q813-706 844-633.98q31 72.03 31 153.92 0 80.97-31.01 153.13-31.02 72.15-84.5 125.79Q706-147.5 633.98-116.25 561.95-85 480.06-85Zm-.09-57.5q140.53 0 239.03-98.97 98.5-98.96 98.5-238.5 0-140.53-98.47-239.03-98.46-98.5-239-98.5-139.53 0-238.53 98.47-99 98.46-99 239 0 139.53 98.97 238.53 98.96 99 238.5 99ZM480-480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 729 B |
+1
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M480.06-85q-80.97 0-153.13-31.26-72.15-31.27-125.79-85Q147.5-255 116.25-327.02 85-399.05 85-479.94q0-81.97 31.26-154.13 31.27-72.15 85-125.54Q255-813 327.02-844q72.03-31 152.92-31 81.97 0 154.13 31.13 72.17 31.13 125.55 84.5Q813-706 844-633.98q31 72.03 31 153.92 0 80.97-31.01 153.13-31.02 72.15-84.5 125.79Q706-147.5 633.98-116.25 561.95-85 480.06-85Zm-.09-57.5q140.53 0 239.03-98.97 98.5-98.96 98.5-238.5 0-140.53-98.47-239.03-98.46-98.5-239-98.5-139.53 0-238.53 98.47-99 98.46-99 239 0 139.53 98.97 238.53 98.96 99 238.5 99ZM480-480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 661 B |
Reference in New Issue
Block a user