Skip to content

Commit

Permalink
Add voice message 'hold to record' tooltip (#1710)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: ElementBot <[email protected]>
  • Loading branch information
jonnyandrew and ElementBot authored Nov 2, 2023
1 parent 8cc541b commit 5080df3
Show file tree
Hide file tree
Showing 18 changed files with 263 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.element.android.libraries.designsystem.components.tooltip

import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TooltipDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupPositionProvider

object ElementTooltipDefaults {
/**
* Creates a [PopupPositionProvider] that allows adding padding between the edge of the
* window and the tooltip.
*
* It is a wrapper around [TooltipDefaults.rememberPlainTooltipPositionProvider] and is
* designed for use with a [PlainTooltip].
*
* @param spacingBetweenTooltipAndAnchor the spacing between the tooltip and the anchor.
* @param windowPadding the padding between the tooltip and the edge of the window.
*
* @return a [PopupPositionProvider].
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun rememberPlainTooltipPositionProvider(
spacingBetweenTooltipAndAnchor: Dp = 8.dp,
windowPadding: Dp = 12.dp,
): PopupPositionProvider {
val windowPaddingPx = with(LocalDensity.current) { windowPadding.roundToPx() }
val plainTooltipPositionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(
spacingBetweenTooltipAndAnchor = spacingBetweenTooltipAndAnchor,
)
return remember(windowPaddingPx, plainTooltipPositionProvider) {
object : PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect,
windowSize: IntSize,
layoutDirection: LayoutDirection,
popupContentSize: IntSize
): IntOffset = plainTooltipPositionProvider
.calculatePosition(
anchorBounds = anchorBounds,
windowSize = windowSize,
layoutDirection = layoutDirection,
popupContentSize = popupContentSize
)
.let {
val maxX = windowSize.width - popupContentSize.width - windowPaddingPx
val maxY = windowSize.height - popupContentSize.height - windowPaddingPx
if (maxX <= windowPaddingPx || maxY <= windowPaddingPx) {
return@let it
}
IntOffset(
x = it.x.coerceIn(
minimumValue = windowPaddingPx,
maximumValue = maxX,
),
y = it.y.coerceIn(
minimumValue = windowPaddingPx,
maximumValue = maxY,
)
)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.element.android.libraries.designsystem.components.tooltip

import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TooltipDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import io.element.android.libraries.theme.ElementTheme
import androidx.compose.material3.PlainTooltip as M3PlainTooltip

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PlainTooltip(
modifier: Modifier = Modifier,
contentColor: Color = ElementTheme.colors.textOnSolidPrimary,
containerColor: Color = ElementTheme.colors.bgActionPrimaryRest,
shape: Shape = TooltipDefaults.plainTooltipContainerShape,
content: @Composable () -> Unit,
) = M3PlainTooltip(
modifier = modifier,
contentColor = contentColor,
containerColor = containerColor,
shape = shape,
content = content,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.element.android.libraries.designsystem.components.tooltip

import androidx.compose.material3.TooltipState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.material3.TooltipBox as M3TooltipBox

@Composable
fun TooltipBox(
positionProvider: PopupPositionProvider,
tooltip: @Composable () -> Unit,
state: TooltipState,
modifier: Modifier = Modifier,
focusable: Boolean = true,
enableUserInput: Boolean = true,
content: @Composable () -> Unit,
) = M3TooltipBox(
positionProvider = positionProvider,
tooltip = tooltip,
state = state,
modifier = modifier,
focusable = focusable,
enableUserInput = enableUserInput,
content = content,
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,50 @@
* limitations under the License.
*/

@file:OptIn(ExperimentalMaterial3Api::class)

package io.element.android.libraries.textcomposer.components

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TooltipState
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.tooltip.ElementTooltipDefaults
import io.element.android.libraries.designsystem.components.tooltip.PlainTooltip
import io.element.android.libraries.designsystem.components.tooltip.TooltipBox
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.textcomposer.R
import io.element.android.libraries.textcomposer.utils.PressState
import io.element.android.libraries.textcomposer.utils.PressStateEffects
import io.element.android.libraries.textcomposer.utils.rememberPressState
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun RecordButton(
modifier: Modifier = Modifier,
initialTooltipIsVisible: Boolean = false,
onPressStart: () -> Unit = {},
onLongPressEnd: () -> Unit = {},
onTap: () -> Unit = {},
Expand All @@ -54,6 +70,10 @@ internal fun RecordButton(
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}

val tooltipState = rememberTooltipState(
initialIsVisible = initialTooltipIsVisible
)

PressStateEffects(
pressState = pressState.value,
onPressStart = {
Expand All @@ -67,26 +87,34 @@ internal fun RecordButton(
onTap = {
onTap()
performHapticFeedback()
coroutineScope.launch { tooltipState.show() }
},
)

RecordButtonView(
isPressed = pressState.value is PressState.Pressing,
modifier = modifier
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
coroutineScope.launch {
when (event.type) {
PointerEventType.Press -> pressState.press()
PointerEventType.Release -> pressState.release()
Box(modifier = modifier) {
HoldToRecordTooltip(
tooltipState = tooltipState,
spacingBetweenTooltipAndAnchor = 0.dp, // Accounts for the 48.dp size of the record button
anchor = {
RecordButtonView(
isPressed = pressState.value is PressState.Pressing,
modifier = Modifier
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
coroutineScope.launch {
when (event.type) {
PointerEventType.Press -> pressState.press()
PointerEventType.Release -> pressState.release()
}
}
}
}
}
}
}
)
}
)
)
}
}

@Composable
Expand All @@ -112,6 +140,34 @@ private fun RecordButtonView(
}
}

@Composable
private fun HoldToRecordTooltip(
tooltipState: TooltipState,
spacingBetweenTooltipAndAnchor: Dp,
modifier: Modifier = Modifier,
anchor: @Composable () -> Unit,
) {
TooltipBox(
positionProvider = ElementTooltipDefaults.rememberPlainTooltipPositionProvider(
spacingBetweenTooltipAndAnchor = spacingBetweenTooltipAndAnchor,
),
tooltip = {
PlainTooltip {
Text(
text = stringResource(R.string.screen_room_voice_message_tooltip),
color = ElementTheme.colors.textOnSolidPrimary,
style = ElementTheme.typography.fontBodySmMedium,
)
}
},
state = tooltipState,
modifier = modifier,
focusable = false,
enableUserInput = false,
content = anchor,
)
}

@PreviewsDayNight
@Composable
internal fun RecordButtonPreview() = ElementPreview {
Expand All @@ -121,3 +177,13 @@ internal fun RecordButtonPreview() = ElementPreview {
}
}

@PreviewsDayNight
@Composable
internal fun HoldToRecordTooltipPreview() = ElementPreview {
Box(modifier = Modifier.fillMaxSize()) {
RecordButton(
modifier = Modifier.align(Alignment.BottomEnd),
initialTooltipIsVisible = true,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@
<string name="rich_text_editor_unindent">"Unindent"</string>
<string name="rich_text_editor_url_placeholder">"Link"</string>
<string name="rich_text_editor_a11y_add_attachment">"Add attachment"</string>
<string name="screen_room_voice_message_tooltip">"Hold to record"</string>
</resources>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion tools/localazy/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
{
"name": ":libraries:textcomposer:impl",
"includeRegex": [
"rich_text_editor.*"
"rich_text_editor.*",
".*voice_message_tooltip"
]
},
{
Expand Down

0 comments on commit 5080df3

Please sign in to comment.