mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-30 22:55:48 +00:00
android, desktop: content filter in chats (#6594)
* android, desktop: content filter in chats * fix command * fix * show content filter menu in search * show end call in app bar during active call with the current contact
This commit is contained in:
@@ -533,7 +533,7 @@ struct ChatView: View {
|
||||
if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||
endCallButton(call)
|
||||
} else {
|
||||
contentFilterMenu()
|
||||
contentFilterMenu(withLabel: false)
|
||||
}
|
||||
Menu {
|
||||
if callsPrefEnabled && chatModel.activeCall == nil {
|
||||
@@ -551,7 +551,7 @@ struct ChatView: View {
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
}
|
||||
if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||
contentFilterMenu()
|
||||
contentFilterMenu(withLabel: true)
|
||||
}
|
||||
searchButton()
|
||||
ToggleNtfsButton(chat: chat)
|
||||
@@ -562,7 +562,7 @@ struct ChatView: View {
|
||||
}
|
||||
case let .group(groupInfo, _):
|
||||
HStack {
|
||||
contentFilterMenu()
|
||||
contentFilterMenu(withLabel: false)
|
||||
Menu {
|
||||
if groupInfo.canAddMembers {
|
||||
if (chat.chatInfo.incognito) {
|
||||
@@ -588,7 +588,7 @@ struct ChatView: View {
|
||||
}
|
||||
case .local:
|
||||
HStack {
|
||||
contentFilterMenu()
|
||||
contentFilterMenu(withLabel: false)
|
||||
searchButton()
|
||||
}
|
||||
default:
|
||||
@@ -1288,7 +1288,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func contentFilterMenu() -> some View {
|
||||
private func contentFilterMenu(withLabel: Bool) -> some View {
|
||||
Menu {
|
||||
ForEach(availableContent, id: \.self) { type in
|
||||
Button {
|
||||
@@ -1305,7 +1305,12 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "photo.on.rectangle.angled")
|
||||
let icon = contentFilter == nil ? "photo.on.rectangle" : "photo.on.rectangle.fill"
|
||||
if withLabel {
|
||||
Label("Filter", systemImage: icon)
|
||||
} else {
|
||||
Image(systemName: icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1316,6 +1321,7 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
private func setContentFilter(_ type: ContentFilter) {
|
||||
if (contentFilter == type) { return }
|
||||
contentFilter = type
|
||||
showSearch = true
|
||||
searchText = ""
|
||||
|
||||
1
apps/multiplatform/.gitignore
vendored
1
apps/multiplatform/.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
*.iml
|
||||
.gradle
|
||||
.kotlin
|
||||
/local.properties
|
||||
/.idea
|
||||
!/.idea/codeStyles/*
|
||||
|
||||
@@ -3735,7 +3735,7 @@ sealed class CC {
|
||||
}
|
||||
"/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||
}
|
||||
is ApiGetChatContentTypes -> "/_get content types ${chatRef(type, id, scope)})"
|
||||
is ApiGetChatContentTypes -> "/_get content types ${chatRef(type, id, scope)}"
|
||||
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id, scope)} $itemId"
|
||||
is ApiSendMessages -> {
|
||||
val msgs = json.encodeToString(composedMessages)
|
||||
|
||||
@@ -27,11 +27,12 @@ suspend fun apiLoadMessages(
|
||||
chatType: ChatType,
|
||||
apiId: Long,
|
||||
pagination: ChatPagination,
|
||||
contentTag: MsgContentTag? = null,
|
||||
search: String = "",
|
||||
openAroundItemId: Long? = null,
|
||||
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
|
||||
) = coroutineScope {
|
||||
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), chatsCtx.contentTag, pagination, search) ?: return@coroutineScope
|
||||
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), contentTag ?: chatsCtx.contentTag, pagination, search) ?: return@coroutineScope
|
||||
// For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes
|
||||
/** When [openAroundItemId] is provided, chatId can be different too */
|
||||
if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last && openAroundItemId == null)
|
||||
|
||||
@@ -45,6 +45,7 @@ import chat.simplex.common.views.newchat.ContactConnectionInfoView
|
||||
import chat.simplex.common.views.newchat.alertProfileImageSize
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.*
|
||||
@@ -144,6 +145,9 @@ fun ChatView(
|
||||
val scope = rememberCoroutineScope()
|
||||
val selectedChatItems = rememberSaveable { mutableStateOf(null as Set<Long>?) }
|
||||
val showCommandsMenu = rememberSaveable { mutableStateOf(false) }
|
||||
val contentFilter = rememberSaveable { mutableStateOf<ContentFilter?>(null) }
|
||||
val availableContent = remember { mutableStateOf<List<ContentFilter>>(ContentFilter.initialList) }
|
||||
|
||||
if (appPlatform.isAndroid) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
@@ -170,7 +174,12 @@ fun ChatView(
|
||||
}
|
||||
showSearch.value = false
|
||||
searchText.value = ""
|
||||
contentFilter.value = null
|
||||
availableContent.value = ContentFilter.initialList
|
||||
selectedChatItems.value = null
|
||||
if (chatsCtx.secondaryContextFilter == null) {
|
||||
updateAvailableContent(chatRh, activeChat, availableContent)
|
||||
}
|
||||
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.activeConn != null) {
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
|
||||
@@ -229,11 +238,11 @@ fun ChatView(
|
||||
val sameText = searchText.value == value
|
||||
// showSearch can be false with empty text when it was closed manually after clicking on message from search to load .around it
|
||||
// (required on Android to have this check to prevent call to search with old text)
|
||||
val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.secondaryContextFilter == null
|
||||
val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.secondaryContextFilter == null && contentFilter.value == null
|
||||
val c = chatModel.getChat(chatInfo.id)
|
||||
if (sameText || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged
|
||||
if ((sameText && contentFilter.value == null) || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged
|
||||
withBGApi {
|
||||
apiFindMessages(chatsCtx, c, value)
|
||||
apiFindMessages(chatsCtx, c, contentFilter.value?.contentTag, value)
|
||||
searchText.value = value
|
||||
}
|
||||
}
|
||||
@@ -486,7 +495,7 @@ fun ChatView(
|
||||
val c = chatModel.getChat(chatId)
|
||||
if (chatModel.chatId.value != chatId) return@ChatLayout
|
||||
if (c != null) {
|
||||
apiLoadMessages(chatsCtx, c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, searchText.value, null, visibleItemIndexes)
|
||||
apiLoadMessages(chatsCtx, c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, contentFilter.value?.contentTag, searchText.value, null, visibleItemIndexes)
|
||||
}
|
||||
},
|
||||
deleteMessage = { itemId, mode ->
|
||||
@@ -742,14 +751,23 @@ fun ChatView(
|
||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) },
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
closeSearch = {
|
||||
onSearchValueChanged("")
|
||||
showSearch.value = false
|
||||
searchText.value = ""
|
||||
contentFilter.value = null
|
||||
// Update available content types when search closes
|
||||
if (chatsCtx.secondaryContextFilter == null) {
|
||||
updateAvailableContent(chatRh, activeChat, availableContent)
|
||||
}
|
||||
},
|
||||
onComposed,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools.get(),
|
||||
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
||||
showSearch = showSearch,
|
||||
showCommandsMenu = showCommandsMenu
|
||||
showCommandsMenu = showCommandsMenu,
|
||||
contentFilter = contentFilter,
|
||||
availableContent = availableContent,
|
||||
searchPlaceholder = contentFilter.value?.searchPlaceholder?.let { generalGetString(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -785,6 +803,23 @@ fun ChatView(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAvailableContent(chatRh: Long?, activeChat: State<Chat?>, availableContent: MutableState<List<ContentFilter>>) {
|
||||
withBGApi {
|
||||
Log.e(TAG, "updateAvailableContent")
|
||||
val chatInfo = activeChat.value?.chatInfo
|
||||
if (chatInfo == null) return@withBGApi
|
||||
val types = chatModel.controller.apiGetChatContentTypes(chatRh, chatInfo.chatType, chatInfo.apiId, null)
|
||||
if (activeChat.value?.chatInfo?.id != chatInfo.id) return@withBGApi
|
||||
if (types == null) {
|
||||
availableContent.value = ContentFilter.entries
|
||||
} else {
|
||||
val typeSet: Set<MsgContentTag> = types.union(ContentFilter.alwaysShow)
|
||||
Log.e(TAG, "updateAvailableContent $typeSet")
|
||||
availableContent.value = ContentFilter.entries.filter { it -> typeSet.contains(it.contentTag) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectingText(chatInfo: ChatInfo): String? {
|
||||
return when (chatInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
@@ -879,7 +914,10 @@ fun ChatLayout(
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean,
|
||||
showSearch: MutableState<Boolean>,
|
||||
showCommandsMenu: MutableState<Boolean>
|
||||
showCommandsMenu: MutableState<Boolean>,
|
||||
contentFilter: MutableState<ContentFilter?>,
|
||||
availableContent: State<List<ContentFilter>>,
|
||||
searchPlaceholder: String?
|
||||
) {
|
||||
val chatInfo = remember { derivedStateOf { chat.value?.chatInfo } }
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -1063,7 +1101,7 @@ fun ChatLayout(
|
||||
Box {
|
||||
if (selectedChatItems.value == null) {
|
||||
if (chatInfo != null) {
|
||||
ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
||||
ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch, contentFilter, availableContent, searchPlaceholder)
|
||||
}
|
||||
} else {
|
||||
SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
|
||||
@@ -1096,10 +1134,14 @@ fun BoxScope.ChatInfoToolbar(
|
||||
openGroupLink: (GroupInfo) -> Unit,
|
||||
changeNtfsState: (MsgFilter, currentValue: MutableState<MsgFilter>) -> Unit,
|
||||
onSearchValueChanged: (String) -> Unit,
|
||||
showSearch: MutableState<Boolean>
|
||||
showSearch: MutableState<Boolean>,
|
||||
contentFilter: MutableState<ContentFilter?>,
|
||||
availableContent: State<List<ContentFilter>>,
|
||||
searchPlaceholder: String?
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val showMenu = rememberSaveable { mutableStateOf(false) }
|
||||
val showContentFilterMenu = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val onBackClicked = {
|
||||
if (!showSearch.value) {
|
||||
@@ -1107,6 +1149,7 @@ fun BoxScope.ChatInfoToolbar(
|
||||
} else {
|
||||
onSearchValueChanged("")
|
||||
showSearch.value = false
|
||||
contentFilter.value = null
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid && chatsCtx.secondaryContextFilter == null) {
|
||||
@@ -1115,104 +1158,141 @@ fun BoxScope.ChatInfoToolbar(
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val menuItems = arrayListOf<@Composable () -> Unit>()
|
||||
val activeCall by remember { chatModel.activeCall }
|
||||
if (chatInfo is ChatInfo.Local) {
|
||||
barButtons.add {
|
||||
IconButton(
|
||||
{
|
||||
showMenu.value = false
|
||||
showSearch.value = true
|
||||
}, enabled = chatInfo.noteFolder.ready
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_search),
|
||||
stringResource(MR.strings.search_verb).capitalize(Locale.current),
|
||||
tint = if (chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = {
|
||||
showMenu.value = false
|
||||
showSearch.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (chatInfo is ChatInfo.Direct && chatInfo.contact.mergedPreferences.calls.enabled.forUser) {
|
||||
if (activeCall == null) {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
}, enabled = chatInfo.contact.ready && chatInfo.contact.active
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_500),
|
||||
stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current),
|
||||
tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (activeCall?.contact?.id == chatInfo.id && appPlatform.isDesktop) {
|
||||
barButtons.add {
|
||||
val call = remember { chatModel.activeCall }.value
|
||||
val connectedAt = call?.connectedAt
|
||||
if (connectedAt != null) {
|
||||
val time = remember { mutableStateOf(durationText(0)) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
time.value = durationText((Clock.System.now() - connectedAt).inWholeSeconds.toInt())
|
||||
delay(250)
|
||||
}
|
||||
}
|
||||
val sp50 = with(LocalDensity.current) { 50.sp.toDp() }
|
||||
Text(time.value, Modifier.widthIn(min = sp50))
|
||||
}
|
||||
}
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
endCall()
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_end_filled),
|
||||
null,
|
||||
tint = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chatInfo.contact.ready && chatInfo.contact.active && activeCall == null) {
|
||||
val showContentFilterButton = availableContent.value.isNotEmpty()
|
||||
val activeCallInChat = chatInfo is ChatInfo.Direct && activeCall?.contact?.id == chatInfo.id
|
||||
|
||||
// Content filter button - shown in bar, or moved to menu during active call
|
||||
if (showContentFilterButton) {
|
||||
val enabled = chatInfo !is ChatInfo.Local || chatInfo.noteFolder.ready
|
||||
if (activeCallInChat) {
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canAddMembers) {
|
||||
if (!chatInfo.incognito) {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
addMembers(chatInfo.groupInfo)
|
||||
}) {
|
||||
Icon(painterResource(MR.images.ic_person_add_500), stringResource(MR.strings.icon_descr_add_members), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
ItemAction(
|
||||
stringResource(MR.strings.content_filter_menu_item),
|
||||
painterResource(MR.images.ic_photo_library),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
showContentFilterMenu.value = true
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
openGroupLink(chatInfo.groupInfo)
|
||||
}) {
|
||||
Icon(painterResource(MR.images.ic_add_link), stringResource(MR.strings.group_link), tint = MaterialTheme.colors.primary)
|
||||
IconButton(
|
||||
{ showContentFilterMenu.value = true },
|
||||
enabled = enabled
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_photo_library),
|
||||
null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chat-type specific buttons
|
||||
when (chatInfo) {
|
||||
is ChatInfo.Local -> {
|
||||
barButtons.add {
|
||||
IconButton(
|
||||
{
|
||||
showMenu.value = false
|
||||
showSearch.value = true
|
||||
}, enabled = chatInfo.noteFolder.ready
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_search),
|
||||
stringResource(MR.strings.search_verb).capitalize(Locale.current),
|
||||
tint = if (chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChatInfo.Direct -> {
|
||||
if (activeCall?.contact?.id == chatInfo.id) {
|
||||
if (appPlatform.isDesktop) {
|
||||
barButtons.add {
|
||||
val call = remember { chatModel.activeCall }.value
|
||||
val connectedAt = call?.connectedAt
|
||||
if (connectedAt != null) {
|
||||
val time = remember { mutableStateOf(durationText(0)) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
time.value = durationText((Clock.System.now() - connectedAt).inWholeSeconds.toInt())
|
||||
delay(250)
|
||||
}
|
||||
}
|
||||
val sp50 = with(LocalDensity.current) { 50.sp.toDp() }
|
||||
Text(time.value, Modifier.widthIn(min = sp50))
|
||||
}
|
||||
}
|
||||
}
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
endCall()
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_end_filled),
|
||||
null,
|
||||
tint = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Call buttons moved to menu
|
||||
if (chatInfo.contact.mergedPreferences.calls.enabled.forUser && chatInfo.contact.ready && chatInfo.contact.active && activeCall == null) {
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), painterResource(MR.images.ic_call_500), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
})
|
||||
}
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
}
|
||||
}
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = {
|
||||
showMenu.value = false
|
||||
showSearch.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
is ChatInfo.Group -> {
|
||||
// Add members / group link moved to menu
|
||||
if (chatInfo.groupInfo.canAddMembers) {
|
||||
if (!chatInfo.incognito) {
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_add_members), painterResource(MR.images.ic_person_add_500), onClick = {
|
||||
showMenu.value = false
|
||||
addMembers(chatInfo.groupInfo)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.group_link), painterResource(MR.images.ic_add_link), onClick = {
|
||||
showMenu.value = false
|
||||
openGroupLink(chatInfo.groupInfo)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = {
|
||||
showMenu.value = false
|
||||
showSearch.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
val enableNtfs = chatInfo.chatSettings?.enableNtfs
|
||||
if (((chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) || chatInfo is ChatInfo.Group) && enableNtfs != null) {
|
||||
val ntfMode = remember { mutableStateOf(enableNtfs) }
|
||||
@@ -1242,13 +1322,27 @@ fun BoxScope.ChatInfoToolbar(
|
||||
}
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val chatBottomBar = remember { appPrefs.chatBottomBar.state }
|
||||
val searchTrailingContent: @Composable (() -> Unit)? = if (showContentFilterButton) {{
|
||||
IconButton({ showContentFilterMenu.value = true }) {
|
||||
Icon(
|
||||
painterResource(if (contentFilter.value == null) MR.images.ic_photo_library else MR.images.ic_photo_library_filled),
|
||||
null,
|
||||
Modifier.padding(4.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}} else null
|
||||
|
||||
DefaultAppBar(
|
||||
navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } },
|
||||
title = { ChatInfoToolbarTitle(chatInfo) },
|
||||
onTitleClick = if (chatInfo is ChatInfo.Local) null else info,
|
||||
showSearch = showSearch.value,
|
||||
searchAlwaysVisible = contentFilter.value != null,
|
||||
onTop = !oneHandUI.value || !chatBottomBar.value,
|
||||
searchPlaceholder = searchPlaceholder,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
searchTrailingContent = searchTrailingContent,
|
||||
buttons = { barButtons.forEach { it() } }
|
||||
)
|
||||
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
|
||||
@@ -1269,6 +1363,65 @@ fun BoxScope.ChatInfoToolbar(
|
||||
menuItems.forEach { it() }
|
||||
}
|
||||
}
|
||||
val contentFilterWidth = remember { mutableStateOf(250.dp) }
|
||||
val contentFilterHeight = remember { mutableStateOf(0.dp) }
|
||||
DefaultDropdownMenu(
|
||||
showContentFilterMenu,
|
||||
modifier = Modifier.onSizeChanged { with(density) {
|
||||
contentFilterWidth.value = it.width.toDp().coerceAtLeast(250.dp)
|
||||
if (oneHandUI.value && chatBottomBar.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) contentFilterHeight.value = it.height.toDp()
|
||||
} },
|
||||
offset = DpOffset(-contentFilterWidth.value, if (oneHandUI.value && chatBottomBar.value) -contentFilterHeight.value else AppBarHeight)
|
||||
) {
|
||||
val contentFilterMenuItems: List<@Composable () -> Unit> = buildList {
|
||||
availableContent.value.forEach { filter ->
|
||||
val isSelected = contentFilter.value == filter
|
||||
add {
|
||||
ItemAction(
|
||||
stringResource(filter.label),
|
||||
painterResource(if (isSelected) filter.iconFilled else filter.icon),
|
||||
color = if (isSelected) MaterialTheme.colors.primary else Color.Unspecified,
|
||||
onClick = {
|
||||
showContentFilterMenu.value = false
|
||||
if (contentFilter.value == filter) return@ItemAction
|
||||
contentFilter.value = filter
|
||||
showSearch.value = true
|
||||
scope.launch {
|
||||
val c = chatModel.getChat(chatInfo.id)
|
||||
if (c != null) {
|
||||
apiFindMessages(chatsCtx, c, filter.contentTag, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showSearch.value) {
|
||||
add {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.content_filter_all_messages),
|
||||
painterResource(MR.images.ic_forum),
|
||||
onClick = {
|
||||
showContentFilterMenu.value = false
|
||||
contentFilter.value = null
|
||||
showSearch.value = false
|
||||
scope.launch {
|
||||
val c = chatModel.getChat(chatInfo.id)
|
||||
if (c != null) {
|
||||
apiFindMessages(chatsCtx, c, null, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oneHandUI.value && chatBottomBar.value) {
|
||||
contentFilterMenuItems.asReversed().forEach { it() }
|
||||
} else {
|
||||
contentFilterMenuItems.forEach { it() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3425,7 +3578,10 @@ fun PreviewChatLayout() {
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
showSearch = remember { mutableStateOf(false) },
|
||||
showCommandsMenu = remember { mutableStateOf(false) }
|
||||
showCommandsMenu = remember { mutableStateOf(false) },
|
||||
contentFilter = remember { mutableStateOf(null) },
|
||||
availableContent = remember { mutableStateOf(ContentFilter.initialList) },
|
||||
searchPlaceholder = null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3505,7 +3661,30 @@ fun PreviewGroupChatLayout() {
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
showSearch = remember { mutableStateOf(false) },
|
||||
showCommandsMenu = remember { mutableStateOf(false) }
|
||||
showCommandsMenu = remember { mutableStateOf(false) },
|
||||
contentFilter = remember { mutableStateOf(null) },
|
||||
availableContent = remember { mutableStateOf(ContentFilter.initialList) },
|
||||
searchPlaceholder = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ContentFilter(
|
||||
val contentTag: MsgContentTag,
|
||||
val label: StringResource,
|
||||
val searchPlaceholder: StringResource,
|
||||
val icon: ImageResource,
|
||||
val iconFilled: ImageResource
|
||||
) {
|
||||
Images(MsgContentTag.Image, MR.strings.content_filter_images, MR.strings.placeholder_search_images, MR.images.ic_image, MR.images.ic_image_filled),
|
||||
Videos(MsgContentTag.Video, MR.strings.content_filter_videos, MR.strings.placeholder_search_videos, MR.images.ic_videocam, MR.images.ic_videocam_filled),
|
||||
Voice(MsgContentTag.Voice, MR.strings.content_filter_voice_messages, MR.strings.placeholder_search_voice_messages, MR.images.ic_mic, MR.images.ic_mic_filled),
|
||||
Files(MsgContentTag.File, MR.strings.content_filter_files, MR.strings.placeholder_search_files, MR.images.ic_draft, MR.images.ic_draft_filled),
|
||||
Links(MsgContentTag.Link, MR.strings.content_filter_links, MR.strings.placeholder_search_links, MR.images.ic_link, MR.images.ic_link);
|
||||
|
||||
companion object {
|
||||
val alwaysShow: Set<MsgContentTag> = setOf(MsgContentTag.Image, MsgContentTag.Link)
|
||||
|
||||
val initialList: List<ContentFilter> = listOf(ContentFilter.Images, ContentFilter.Files, ContentFilter.Links)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +228,7 @@ suspend fun openChat(
|
||||
} else {
|
||||
ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
|
||||
},
|
||||
contentTag = null,
|
||||
"",
|
||||
openAroundItemId
|
||||
)
|
||||
@@ -241,11 +242,12 @@ suspend fun openLoadedChat(chat: Chat) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiFindMessages(chatsCtx: ChatModel.ChatsContext, ch: Chat, search: String) {
|
||||
suspend fun apiFindMessages(chatsCtx: ChatModel.ChatsContext, ch: Chat, contentTag: MsgContentTag?, search: String) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatsCtx.chatItems.clearAndNotify()
|
||||
}
|
||||
apiLoadMessages(chatsCtx, ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination = if (search.isNotEmpty()) ChatPagination.Last(ChatPagination.INITIAL_COUNT) else ChatPagination.Initial(ChatPagination.INITIAL_COUNT), search = search)
|
||||
val pagination = if (search.isNotEmpty() || contentTag != null) ChatPagination.Last(ChatPagination.INITIAL_COUNT) else ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
|
||||
apiLoadMessages(chatsCtx, ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination, contentTag, search)
|
||||
}
|
||||
|
||||
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope {
|
||||
|
||||
@@ -29,7 +29,9 @@ fun DefaultAppBar(
|
||||
onTop: Boolean,
|
||||
showSearch: Boolean = false,
|
||||
searchAlwaysVisible: Boolean = false,
|
||||
searchPlaceholder: String? = null,
|
||||
onSearchValueChanged: (String) -> Unit = {},
|
||||
searchTrailingContent: @Composable (() -> Unit)? = null,
|
||||
buttons: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
// If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier
|
||||
@@ -78,7 +80,8 @@ fun DefaultAppBar(
|
||||
AppBar(
|
||||
title = {
|
||||
if (showSearch) {
|
||||
SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = searchAlwaysVisible, reducedCloseButtonPadding = 12.dp, onValueChange = onSearchValueChanged)
|
||||
val placeholder = searchPlaceholder ?: stringResource(MR.strings.search_verb)
|
||||
SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = searchAlwaysVisible, placeholder = placeholder, trailingContent = searchTrailingContent, reducedCloseButtonPadding = 12.dp, onValueChange = onSearchValueChanged)
|
||||
} else if (title != null) {
|
||||
title()
|
||||
} else if (titleText.value.isNotEmpty() && connection != null) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.material.TextFieldDefaults.indicatorLine
|
||||
import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
@@ -112,18 +113,26 @@ fun SearchTextField(
|
||||
placeholder = {
|
||||
Text(placeholder, style = textStyle.copy(color = MaterialTheme.colors.secondary), maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
},
|
||||
trailingIcon = if (searchText.value.text.isNotEmpty()) {{
|
||||
IconButton({
|
||||
if (alwaysVisible) {
|
||||
keyboard?.hide()
|
||||
focusManager.clearFocus()
|
||||
trailingIcon = if (searchText.value.text.isNotEmpty() || trailingContent != null) {{
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.offset(x = 8.dp)
|
||||
) {
|
||||
if (searchText.value.text.isNotEmpty()) {
|
||||
IconButton({
|
||||
if (alwaysVisible) {
|
||||
keyboard?.hide()
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
searchText.value = TextFieldValue("")
|
||||
onValueChange("")
|
||||
}) {
|
||||
Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
searchText.value = TextFieldValue("");
|
||||
onValueChange("")
|
||||
}, Modifier.offset(x = reducedCloseButtonPadding)) {
|
||||
Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary,)
|
||||
trailingContent?.invoke()
|
||||
}
|
||||
}} else trailingContent,
|
||||
}} else null,
|
||||
singleLine = true,
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
|
||||
@@ -368,6 +368,18 @@
|
||||
<string name="edit_verb">Edit</string>
|
||||
<string name="info_menu">Info</string>
|
||||
<string name="search_verb">Search</string>
|
||||
<string name="placeholder_search_images">Search images</string>
|
||||
<string name="placeholder_search_videos">Search videos</string>
|
||||
<string name="placeholder_search_voice_messages">Search voice messages</string>
|
||||
<string name="placeholder_search_files">Search files</string>
|
||||
<string name="placeholder_search_links">Search links</string>
|
||||
<string name="content_filter_images">Images</string>
|
||||
<string name="content_filter_videos">Videos</string>
|
||||
<string name="content_filter_voice_messages">Voice messages</string>
|
||||
<string name="content_filter_files">Files</string>
|
||||
<string name="content_filter_links">Links</string>
|
||||
<string name="content_filter_all_messages">All messages</string>
|
||||
<string name="content_filter_menu_item">Filter</string>
|
||||
<string name="archive_verb">Archive</string>
|
||||
<string name="archive_report">Archive report</string>
|
||||
<string name="archive_reports">Archive reports</string>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000">
|
||||
<path
|
||||
d="M182-124.5q-22.97 0-40.23-17.27Q124.5-159.03 124.5-182v-596q0-22.97 17.27-40.23Q159.03-835.5 182-835.5h596q22.97 0 40.23 17.27Q835.5-800.97 835.5-778v596q0 22.97-17.27 40.23Q800.97-124.5 778-124.5H182Zm58-154h481.5L577-471 446-301.5l-92-125-114 148Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 389 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000">
|
||||
<path
|
||||
d="M345.5-376H731L606.5-544l-103 135-67.5-86.5L345.5-376Zm-88 176q-22.97 0-40.23-17.27Q200-234.53 200-257.5v-560q0-22.97 17.27-40.23Q234.53-875 257.5-875h560q22.97 0 40.23 17.27Q875-840.47 875-817.5v560q0 22.97-17.27 40.23Q840.47-200 817.5-200h-560Zm0-57.5h560v-560h-560v560ZM142.5-85q-22.97 0-40.23-17.27Q85-119.53 85-142.5V-760h57.5v617.5H760V-85H142.5Zm115-732.5v560-560Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 511 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000">
|
||||
<path
|
||||
d="M345.5-376H731L606.5-544l-103 135-67.5-86.5L345.5-376Zm-88 176q-22.97 0-40.23-17.27Q200-234.53 200-257.5v-560q0-22.97 17.27-40.23Q234.53-875 257.5-875h560q22.97 0 40.23 17.27Q875-840.47 875-817.5v560q0 22.97-17.27 40.23Q840.47-200 817.5-200h-560Zm-115 115q-22.97 0-40.23-17.27Q85-119.53 85-142.5V-760h57.5v617.5H760V-85H142.5Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 466 B |
Reference in New Issue
Block a user