mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-31 20:36:19 +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 {
|
if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||||
endCallButton(call)
|
endCallButton(call)
|
||||||
} else {
|
} else {
|
||||||
contentFilterMenu()
|
contentFilterMenu(withLabel: false)
|
||||||
}
|
}
|
||||||
Menu {
|
Menu {
|
||||||
if callsPrefEnabled && chatModel.activeCall == nil {
|
if callsPrefEnabled && chatModel.activeCall == nil {
|
||||||
@@ -551,7 +551,7 @@ struct ChatView: View {
|
|||||||
.disabled(!contact.ready || !contact.active)
|
.disabled(!contact.ready || !contact.active)
|
||||||
}
|
}
|
||||||
if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||||
contentFilterMenu()
|
contentFilterMenu(withLabel: true)
|
||||||
}
|
}
|
||||||
searchButton()
|
searchButton()
|
||||||
ToggleNtfsButton(chat: chat)
|
ToggleNtfsButton(chat: chat)
|
||||||
@@ -562,7 +562,7 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
case let .group(groupInfo, _):
|
case let .group(groupInfo, _):
|
||||||
HStack {
|
HStack {
|
||||||
contentFilterMenu()
|
contentFilterMenu(withLabel: false)
|
||||||
Menu {
|
Menu {
|
||||||
if groupInfo.canAddMembers {
|
if groupInfo.canAddMembers {
|
||||||
if (chat.chatInfo.incognito) {
|
if (chat.chatInfo.incognito) {
|
||||||
@@ -588,7 +588,7 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
case .local:
|
case .local:
|
||||||
HStack {
|
HStack {
|
||||||
contentFilterMenu()
|
contentFilterMenu(withLabel: false)
|
||||||
searchButton()
|
searchButton()
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -1288,7 +1288,7 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func contentFilterMenu() -> some View {
|
private func contentFilterMenu(withLabel: Bool) -> some View {
|
||||||
Menu {
|
Menu {
|
||||||
ForEach(availableContent, id: \.self) { type in
|
ForEach(availableContent, id: \.self) { type in
|
||||||
Button {
|
Button {
|
||||||
@@ -1305,7 +1305,12 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} 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) {
|
private func setContentFilter(_ type: ContentFilter) {
|
||||||
|
if (contentFilter == type) { return }
|
||||||
contentFilter = type
|
contentFilter = type
|
||||||
showSearch = true
|
showSearch = true
|
||||||
searchText = ""
|
searchText = ""
|
||||||
|
|||||||
1
apps/multiplatform/.gitignore
vendored
1
apps/multiplatform/.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
*.iml
|
*.iml
|
||||||
.gradle
|
.gradle
|
||||||
|
.kotlin
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea
|
/.idea
|
||||||
!/.idea/codeStyles/*
|
!/.idea/codeStyles/*
|
||||||
|
|||||||
@@ -3735,7 +3735,7 @@ sealed class CC {
|
|||||||
}
|
}
|
||||||
"/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
"/_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 ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id, scope)} $itemId"
|
||||||
is ApiSendMessages -> {
|
is ApiSendMessages -> {
|
||||||
val msgs = json.encodeToString(composedMessages)
|
val msgs = json.encodeToString(composedMessages)
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ suspend fun apiLoadMessages(
|
|||||||
chatType: ChatType,
|
chatType: ChatType,
|
||||||
apiId: Long,
|
apiId: Long,
|
||||||
pagination: ChatPagination,
|
pagination: ChatPagination,
|
||||||
|
contentTag: MsgContentTag? = null,
|
||||||
search: String = "",
|
search: String = "",
|
||||||
openAroundItemId: Long? = null,
|
openAroundItemId: Long? = null,
|
||||||
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
|
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
|
||||||
) = coroutineScope {
|
) = 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
|
// 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 */
|
/** 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)
|
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.common.views.newchat.alertProfileImageSize
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.ImageResource
|
import dev.icerock.moko.resources.ImageResource
|
||||||
|
import dev.icerock.moko.resources.StringResource
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.datetime.*
|
import kotlinx.datetime.*
|
||||||
@@ -144,6 +145,9 @@ fun ChatView(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val selectedChatItems = rememberSaveable { mutableStateOf(null as Set<Long>?) }
|
val selectedChatItems = rememberSaveable { mutableStateOf(null as Set<Long>?) }
|
||||||
val showCommandsMenu = rememberSaveable { mutableStateOf(false) }
|
val showCommandsMenu = rememberSaveable { mutableStateOf(false) }
|
||||||
|
val contentFilter = rememberSaveable { mutableStateOf<ContentFilter?>(null) }
|
||||||
|
val availableContent = remember { mutableStateOf<List<ContentFilter>>(ContentFilter.initialList) }
|
||||||
|
|
||||||
if (appPlatform.isAndroid) {
|
if (appPlatform.isAndroid) {
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
@@ -170,7 +174,12 @@ fun ChatView(
|
|||||||
}
|
}
|
||||||
showSearch.value = false
|
showSearch.value = false
|
||||||
searchText.value = ""
|
searchText.value = ""
|
||||||
|
contentFilter.value = null
|
||||||
|
availableContent.value = ContentFilter.initialList
|
||||||
selectedChatItems.value = null
|
selectedChatItems.value = null
|
||||||
|
if (chatsCtx.secondaryContextFilter == null) {
|
||||||
|
updateAvailableContent(chatRh, activeChat, availableContent)
|
||||||
|
}
|
||||||
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.activeConn != null) {
|
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.activeConn != null) {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val r = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
|
val r = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
|
||||||
@@ -229,11 +238,11 @@ fun ChatView(
|
|||||||
val sameText = searchText.value == value
|
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
|
// 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)
|
// (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)
|
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 {
|
withBGApi {
|
||||||
apiFindMessages(chatsCtx, c, value)
|
apiFindMessages(chatsCtx, c, contentFilter.value?.contentTag, value)
|
||||||
searchText.value = value
|
searchText.value = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,7 +495,7 @@ fun ChatView(
|
|||||||
val c = chatModel.getChat(chatId)
|
val c = chatModel.getChat(chatId)
|
||||||
if (chatModel.chatId.value != chatId) return@ChatLayout
|
if (chatModel.chatId.value != chatId) return@ChatLayout
|
||||||
if (c != null) {
|
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 ->
|
deleteMessage = { itemId, mode ->
|
||||||
@@ -742,14 +751,23 @@ fun ChatView(
|
|||||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) },
|
changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) },
|
||||||
onSearchValueChanged = onSearchValueChanged,
|
onSearchValueChanged = onSearchValueChanged,
|
||||||
closeSearch = {
|
closeSearch = {
|
||||||
|
onSearchValueChanged("")
|
||||||
showSearch.value = false
|
showSearch.value = false
|
||||||
searchText.value = ""
|
searchText.value = ""
|
||||||
|
contentFilter.value = null
|
||||||
|
// Update available content types when search closes
|
||||||
|
if (chatsCtx.secondaryContextFilter == null) {
|
||||||
|
updateAvailableContent(chatRh, activeChat, availableContent)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onComposed,
|
onComposed,
|
||||||
developerTools = chatModel.controller.appPrefs.developerTools.get(),
|
developerTools = chatModel.controller.appPrefs.developerTools.get(),
|
||||||
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
|
||||||
showSearch = showSearch,
|
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? {
|
private fun connectingText(chatInfo: ChatInfo): String? {
|
||||||
return when (chatInfo) {
|
return when (chatInfo) {
|
||||||
is ChatInfo.Direct ->
|
is ChatInfo.Direct ->
|
||||||
@@ -879,7 +914,10 @@ fun ChatLayout(
|
|||||||
developerTools: Boolean,
|
developerTools: Boolean,
|
||||||
showViaProxy: Boolean,
|
showViaProxy: Boolean,
|
||||||
showSearch: MutableState<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 chatInfo = remember { derivedStateOf { chat.value?.chatInfo } }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -1063,7 +1101,7 @@ fun ChatLayout(
|
|||||||
Box {
|
Box {
|
||||||
if (selectedChatItems.value == null) {
|
if (selectedChatItems.value == null) {
|
||||||
if (chatInfo != 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 {
|
} else {
|
||||||
SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
|
SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
|
||||||
@@ -1096,10 +1134,14 @@ fun BoxScope.ChatInfoToolbar(
|
|||||||
openGroupLink: (GroupInfo) -> Unit,
|
openGroupLink: (GroupInfo) -> Unit,
|
||||||
changeNtfsState: (MsgFilter, currentValue: MutableState<MsgFilter>) -> Unit,
|
changeNtfsState: (MsgFilter, currentValue: MutableState<MsgFilter>) -> Unit,
|
||||||
onSearchValueChanged: (String) -> Unit,
|
onSearchValueChanged: (String) -> Unit,
|
||||||
showSearch: MutableState<Boolean>
|
showSearch: MutableState<Boolean>,
|
||||||
|
contentFilter: MutableState<ContentFilter?>,
|
||||||
|
availableContent: State<List<ContentFilter>>,
|
||||||
|
searchPlaceholder: String?
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val showMenu = rememberSaveable { mutableStateOf(false) }
|
val showMenu = rememberSaveable { mutableStateOf(false) }
|
||||||
|
val showContentFilterMenu = rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val onBackClicked = {
|
val onBackClicked = {
|
||||||
if (!showSearch.value) {
|
if (!showSearch.value) {
|
||||||
@@ -1107,6 +1149,7 @@ fun BoxScope.ChatInfoToolbar(
|
|||||||
} else {
|
} else {
|
||||||
onSearchValueChanged("")
|
onSearchValueChanged("")
|
||||||
showSearch.value = false
|
showSearch.value = false
|
||||||
|
contentFilter.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (appPlatform.isAndroid && chatsCtx.secondaryContextFilter == null) {
|
if (appPlatform.isAndroid && chatsCtx.secondaryContextFilter == null) {
|
||||||
@@ -1115,104 +1158,141 @@ fun BoxScope.ChatInfoToolbar(
|
|||||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||||
val menuItems = arrayListOf<@Composable () -> Unit>()
|
val menuItems = arrayListOf<@Composable () -> Unit>()
|
||||||
val activeCall by remember { chatModel.activeCall }
|
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) {
|
val showContentFilterButton = availableContent.value.isNotEmpty()
|
||||||
if (activeCall == null) {
|
val activeCallInChat = chatInfo is ChatInfo.Direct && activeCall?.contact?.id == chatInfo.id
|
||||||
barButtons.add {
|
|
||||||
IconButton({
|
// Content filter button - shown in bar, or moved to menu during active call
|
||||||
showMenu.value = false
|
if (showContentFilterButton) {
|
||||||
startCall(CallMediaType.Audio)
|
val enabled = chatInfo !is ChatInfo.Local || chatInfo.noteFolder.ready
|
||||||
}, enabled = chatInfo.contact.ready && chatInfo.contact.active
|
if (activeCallInChat) {
|
||||||
) {
|
|
||||||
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) {
|
|
||||||
menuItems.add {
|
menuItems.add {
|
||||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
ItemAction(
|
||||||
showMenu.value = false
|
stringResource(MR.strings.content_filter_menu_item),
|
||||||
startCall(CallMediaType.Video)
|
painterResource(MR.images.ic_photo_library),
|
||||||
})
|
onClick = {
|
||||||
}
|
showMenu.value = false
|
||||||
}
|
showContentFilterMenu.value = true
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
barButtons.add {
|
barButtons.add {
|
||||||
IconButton({
|
IconButton(
|
||||||
showMenu.value = false
|
{ showContentFilterMenu.value = true },
|
||||||
openGroupLink(chatInfo.groupInfo)
|
enabled = enabled
|
||||||
}) {
|
) {
|
||||||
Icon(painterResource(MR.images.ic_add_link), stringResource(MR.strings.group_link), tint = MaterialTheme.colors.primary)
|
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
|
val enableNtfs = chatInfo.chatSettings?.enableNtfs
|
||||||
if (((chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) || chatInfo is ChatInfo.Group) && enableNtfs != null) {
|
if (((chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) || chatInfo is ChatInfo.Group) && enableNtfs != null) {
|
||||||
val ntfMode = remember { mutableStateOf(enableNtfs) }
|
val ntfMode = remember { mutableStateOf(enableNtfs) }
|
||||||
@@ -1242,13 +1322,27 @@ fun BoxScope.ChatInfoToolbar(
|
|||||||
}
|
}
|
||||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
val chatBottomBar = remember { appPrefs.chatBottomBar.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(
|
DefaultAppBar(
|
||||||
navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } },
|
navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } },
|
||||||
title = { ChatInfoToolbarTitle(chatInfo) },
|
title = { ChatInfoToolbarTitle(chatInfo) },
|
||||||
onTitleClick = if (chatInfo is ChatInfo.Local) null else info,
|
onTitleClick = if (chatInfo is ChatInfo.Local) null else info,
|
||||||
showSearch = showSearch.value,
|
showSearch = showSearch.value,
|
||||||
|
searchAlwaysVisible = contentFilter.value != null,
|
||||||
onTop = !oneHandUI.value || !chatBottomBar.value,
|
onTop = !oneHandUI.value || !chatBottomBar.value,
|
||||||
|
searchPlaceholder = searchPlaceholder,
|
||||||
onSearchValueChanged = onSearchValueChanged,
|
onSearchValueChanged = onSearchValueChanged,
|
||||||
|
searchTrailingContent = searchTrailingContent,
|
||||||
buttons = { barButtons.forEach { it() } }
|
buttons = { barButtons.forEach { it() } }
|
||||||
)
|
)
|
||||||
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
|
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
|
||||||
@@ -1269,6 +1363,65 @@ fun BoxScope.ChatInfoToolbar(
|
|||||||
menuItems.forEach { it() }
|
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,
|
developerTools = false,
|
||||||
showViaProxy = false,
|
showViaProxy = false,
|
||||||
showSearch = remember { mutableStateOf(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,
|
developerTools = false,
|
||||||
showViaProxy = false,
|
showViaProxy = false,
|
||||||
showSearch = remember { mutableStateOf(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 {
|
} else {
|
||||||
ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
|
ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
|
||||||
},
|
},
|
||||||
|
contentTag = null,
|
||||||
"",
|
"",
|
||||||
openAroundItemId
|
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) {
|
withContext(Dispatchers.Main) {
|
||||||
chatsCtx.chatItems.clearAndNotify()
|
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 {
|
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope {
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ fun DefaultAppBar(
|
|||||||
onTop: Boolean,
|
onTop: Boolean,
|
||||||
showSearch: Boolean = false,
|
showSearch: Boolean = false,
|
||||||
searchAlwaysVisible: Boolean = false,
|
searchAlwaysVisible: Boolean = false,
|
||||||
|
searchPlaceholder: String? = null,
|
||||||
onSearchValueChanged: (String) -> Unit = {},
|
onSearchValueChanged: (String) -> Unit = {},
|
||||||
|
searchTrailingContent: @Composable (() -> Unit)? = null,
|
||||||
buttons: @Composable RowScope.() -> Unit = {},
|
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
|
// 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(
|
AppBar(
|
||||||
title = {
|
title = {
|
||||||
if (showSearch) {
|
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) {
|
} else if (title != null) {
|
||||||
title()
|
title()
|
||||||
} else if (titleText.value.isNotEmpty() && connection != null) {
|
} 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.material.TextFieldDefaults.textFieldWithLabelPadding
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
@@ -112,18 +113,26 @@ fun SearchTextField(
|
|||||||
placeholder = {
|
placeholder = {
|
||||||
Text(placeholder, style = textStyle.copy(color = MaterialTheme.colors.secondary), maxLines = 1, overflow = TextOverflow.Ellipsis)
|
Text(placeholder, style = textStyle.copy(color = MaterialTheme.colors.secondary), maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
},
|
},
|
||||||
trailingIcon = if (searchText.value.text.isNotEmpty()) {{
|
trailingIcon = if (searchText.value.text.isNotEmpty() || trailingContent != null) {{
|
||||||
IconButton({
|
Row(
|
||||||
if (alwaysVisible) {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
keyboard?.hide()
|
modifier = Modifier.offset(x = 8.dp)
|
||||||
focusManager.clearFocus()
|
) {
|
||||||
|
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("");
|
trailingContent?.invoke()
|
||||||
onValueChange("")
|
|
||||||
}, Modifier.offset(x = reducedCloseButtonPadding)) {
|
|
||||||
Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary,)
|
|
||||||
}
|
}
|
||||||
}} else trailingContent,
|
}} else null,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|||||||
@@ -368,6 +368,18 @@
|
|||||||
<string name="edit_verb">Edit</string>
|
<string name="edit_verb">Edit</string>
|
||||||
<string name="info_menu">Info</string>
|
<string name="info_menu">Info</string>
|
||||||
<string name="search_verb">Search</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_verb">Archive</string>
|
||||||
<string name="archive_report">Archive report</string>
|
<string name="archive_report">Archive report</string>
|
||||||
<string name="archive_reports">Archive reports</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