mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-28 00:44:27 +00:00
ui: call button in toolbar with audio/video submenu, filter in menu on mobile (#6700)
* ui: call button in toolbar with audio/video submenu, filter in menu on mobile - Call button always outside three-dots menu; tapping it opens a submenu with Audio call and Video call options (both Android/iOS and desktop) - Desktop: two toolbar buttons — Filter + Call - Android/iOS: call button in toolbar; filter options move into the three-dots menu below a divider line - Groups: filter button always in toolbar on all platforms - Edge case: when calls are disabled/unavailable for a contact, the call button is hidden and the filter button is shown in its place in the toolbar instead Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ui: call button before filter button on desktop toolbar --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
Evgeny Poberezkin
parent
427ce1b3ff
commit
b0e6a7bfc1
@@ -533,32 +533,51 @@ struct ChatView: View {
|
||||
case let .direct(contact):
|
||||
HStack {
|
||||
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
|
||||
let canStartCall = callsPrefEnabled && contact.ready && contact.active && chatModel.activeCall == nil
|
||||
if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||
endCallButton(call)
|
||||
} else {
|
||||
contentFilterMenu(withLabel: false)
|
||||
}
|
||||
Menu {
|
||||
if callsPrefEnabled && chatModel.activeCall == nil {
|
||||
} else if canStartCall {
|
||||
// Call button always in toolbar; tap opens Audio/Video submenu
|
||||
Menu {
|
||||
Button {
|
||||
CallController.shared.startCall(contact, .audio)
|
||||
} label: {
|
||||
Label("Audio call", systemImage: "phone")
|
||||
}
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
Button {
|
||||
CallController.shared.startCall(contact, .video)
|
||||
} label: {
|
||||
Label("Video call", systemImage: "video")
|
||||
}
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
}
|
||||
if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||
contentFilterMenu(withLabel: true)
|
||||
} label: {
|
||||
Image(systemName: "phone")
|
||||
}
|
||||
} else if chatModel.activeCall == nil {
|
||||
// Calls unavailable: show filter button in place of call button
|
||||
contentFilterMenu(withLabel: false)
|
||||
}
|
||||
Menu {
|
||||
searchButton()
|
||||
ToggleNtfsButton(chat: chat)
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
// Filter options in menu when call button is shown (or during any active call)
|
||||
if !availableContent.isEmpty && (canStartCall || chatModel.activeCall != nil) {
|
||||
Divider()
|
||||
ForEach(availableContent, id: \.self) { type in
|
||||
Button {
|
||||
setContentFilter(type)
|
||||
} label: {
|
||||
Label(type.label, systemImage: contentFilter == type ? type.iconFilled : type.icon)
|
||||
}
|
||||
}
|
||||
if contentFilter != nil {
|
||||
Button {
|
||||
closeSearch()
|
||||
} label: {
|
||||
Label("All messages", systemImage: "bubble.left.and.text.bubble.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
@@ -591,7 +610,26 @@ struct ChatView: View {
|
||||
}
|
||||
case .local:
|
||||
HStack {
|
||||
contentFilterMenu(withLabel: false)
|
||||
if !availableContent.isEmpty {
|
||||
Menu {
|
||||
ForEach(availableContent, id: \.self) { type in
|
||||
Button {
|
||||
setContentFilter(type)
|
||||
} label: {
|
||||
Label(type.label, systemImage: contentFilter == type ? type.iconFilled : type.icon)
|
||||
}
|
||||
}
|
||||
if contentFilter != nil {
|
||||
Button {
|
||||
closeSearch()
|
||||
} label: {
|
||||
Label("All messages", systemImage: "bubble.left.and.text.bubble.right")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
}
|
||||
searchButton()
|
||||
}
|
||||
default:
|
||||
|
||||
+103
-37
@@ -1145,6 +1145,7 @@ fun BoxScope.ChatInfoToolbar(
|
||||
val scope = rememberCoroutineScope()
|
||||
val showMenu = rememberSaveable { mutableStateOf(false) }
|
||||
val showContentFilterMenu = rememberSaveable { mutableStateOf(false) }
|
||||
val showCallMenu = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val onBackClicked = {
|
||||
if (!showSearch.value) {
|
||||
@@ -1163,35 +1164,28 @@ fun BoxScope.ChatInfoToolbar(
|
||||
val activeCall by remember { chatModel.activeCall }
|
||||
|
||||
val showContentFilterButton = availableContent.value.isNotEmpty()
|
||||
val activeCallInChat = chatInfo is ChatInfo.Direct && activeCall?.contact?.id == chatInfo.id
|
||||
val canStartCall = chatInfo is ChatInfo.Direct &&
|
||||
chatInfo.contact.mergedPreferences.calls.enabled.forUser &&
|
||||
chatInfo.contact.ready &&
|
||||
chatInfo.contact.active &&
|
||||
activeCall == null
|
||||
|
||||
// Content filter button - shown in bar, or moved to menu during active call
|
||||
if (showContentFilterButton) {
|
||||
// Content filter button: always in bar on desktop and for groups; on Android for direct chats it
|
||||
// goes into the three-dots menu UNLESS calls are unavailable, in which case it appears in the bar
|
||||
if (showContentFilterButton && (appPlatform.isDesktop || chatInfo is ChatInfo.Group ||
|
||||
(appPlatform.isAndroid && chatInfo is ChatInfo.Direct && !canStartCall && activeCall == null))) {
|
||||
val enabled = chatInfo !is ChatInfo.Local || chatInfo.noteFolder.ready
|
||||
if (activeCallInChat) {
|
||||
menuItems.add {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.content_filter_menu_item),
|
||||
barButtons.add {
|
||||
IconButton(
|
||||
{ showContentFilterMenu.value = true },
|
||||
enabled = enabled
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_photo_library),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
showContentFilterMenu.value = true
|
||||
}
|
||||
null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
barButtons.add {
|
||||
IconButton(
|
||||
{ showContentFilterMenu.value = true },
|
||||
enabled = enabled
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_photo_library),
|
||||
null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1245,19 +1239,12 @@ fun BoxScope.ChatInfoToolbar(
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
})
|
||||
// Call button always in toolbar; tap opens Audio/Video call submenu
|
||||
if (canStartCall) {
|
||||
barButtons.add(0) {
|
||||
IconButton({ showCallMenu.value = true }) {
|
||||
Icon(painterResource(MR.images.ic_call_500), null, tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
menuItems.add {
|
||||
@@ -1316,6 +1303,53 @@ fun BoxScope.ChatInfoToolbar(
|
||||
}
|
||||
}
|
||||
|
||||
// Android only: for direct/local chats where the filter bar button is NOT shown, filter options go in the three-dots menu separated by a divider
|
||||
if (appPlatform.isAndroid && chatInfo !is ChatInfo.Group && showContentFilterButton &&
|
||||
!(chatInfo is ChatInfo.Direct && !canStartCall && activeCall == null)) {
|
||||
menuItems.add { Divider() }
|
||||
availableContent.value.forEach { filter ->
|
||||
menuItems.add {
|
||||
val isSelected = contentFilter.value == filter
|
||||
ItemAction(
|
||||
stringResource(filter.label),
|
||||
painterResource(if (isSelected) filter.iconFilled else filter.icon),
|
||||
color = if (isSelected) MaterialTheme.colors.primary else Color.Unspecified,
|
||||
onClick = {
|
||||
showMenu.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) {
|
||||
menuItems.add {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.content_filter_all_messages),
|
||||
painterResource(MR.images.ic_forum),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
contentFilter.value = null
|
||||
showSearch.value = false
|
||||
scope.launch {
|
||||
val c = chatModel.getChat(chatInfo.id)
|
||||
if (c != null) {
|
||||
apiFindMessages(chatsCtx, c, null, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (menuItems.isNotEmpty()) {
|
||||
barButtons.add {
|
||||
IconButton({ showMenu.value = true }) {
|
||||
@@ -1425,6 +1459,38 @@ fun BoxScope.ChatInfoToolbar(
|
||||
contentFilterMenuItems.forEach { it() }
|
||||
}
|
||||
}
|
||||
val callMenuWidth = remember { mutableStateOf(250.dp) }
|
||||
val callMenuHeight = remember { mutableStateOf(0.dp) }
|
||||
DefaultDropdownMenu(
|
||||
showCallMenu,
|
||||
modifier = Modifier.onSizeChanged { with(density) {
|
||||
callMenuWidth.value = it.width.toDp().coerceAtLeast(250.dp)
|
||||
if (oneHandUI.value && chatBottomBar.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) callMenuHeight.value = it.height.toDp()
|
||||
} },
|
||||
offset = DpOffset(-callMenuWidth.value, if (oneHandUI.value && chatBottomBar.value) -callMenuHeight.value else AppBarHeight)
|
||||
) {
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
val callMenuItems: List<@Composable () -> Unit> = buildList {
|
||||
add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), painterResource(MR.images.ic_call_500), onClick = {
|
||||
showCallMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
})
|
||||
}
|
||||
add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
showCallMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
}
|
||||
}
|
||||
if (oneHandUI.value && chatBottomBar.value) {
|
||||
callMenuItems.asReversed().forEach { it() }
|
||||
} else {
|
||||
callMenuItems.forEach { it() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user