diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/ElementTooltipDefaults.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/ElementTooltipDefaults.kt
new file mode 100644
index 0000000000..d1713d984c
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/ElementTooltipDefaults.kt
@@ -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,
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/PlainTooltip.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/PlainTooltip.kt
new file mode 100644
index 0000000000..78f381a685
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/PlainTooltip.kt
@@ -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,
+)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/TooltipBox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/TooltipBox.kt
new file mode 100644
index 0000000000..589f1fddcd
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/tooltip/TooltipBox.kt
@@ -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,
+)
diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt
index e76a1b16b0..0a0710095e 100644
--- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt
+++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt
@@ -14,24 +14,38 @@
* 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
@@ -39,9 +53,11 @@ 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 = {},
@@ -54,6 +70,10 @@ internal fun RecordButton(
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
+ val tooltipState = rememberTooltipState(
+ initialIsVisible = initialTooltipIsVisible
+ )
+
PressStateEffects(
pressState = pressState.value,
onPressStart = {
@@ -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
@@ -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 {
@@ -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,
+ )
+ }
+}
diff --git a/libraries/textcomposer/impl/src/main/res/values/localazy.xml b/libraries/textcomposer/impl/src/main/res/values/localazy.xml
index 5017d353f8..8ba2c3b0b3 100644
--- a/libraries/textcomposer/impl/src/main/res/values/localazy.xml
+++ b/libraries/textcomposer/impl/src/main/res/values/localazy.xml
@@ -21,4 +21,5 @@
"Unindent"
"Link"
"Add attachment"
+ "Hold to record"
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_HoldToRecordTooltip-D-13_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_HoldToRecordTooltip-D-13_13_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..2f3dc97e8c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_HoldToRecordTooltip-D-13_13_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e6d75686b1463d11d89e5130b160c3c446f26612e8da44981cc6994687001d80
+size 7093
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_HoldToRecordTooltip-N-13_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_HoldToRecordTooltip-N-13_14_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..ff6633e96a
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_HoldToRecordTooltip-N-13_14_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f8c96b9ce6a8d9136c56789e9e6af25b4259143f6416305c523f4e5224bc8fa8
+size 5898
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-13_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-14_14_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-13_13_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-14_14_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-13_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-14_15_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-13_14_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-14_15_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-14_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-15_15_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-14_14_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-15_15_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-14_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-15_16_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-14_15_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-15_16_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-15_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-16_16_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-15_15_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-16_16_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-15_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-16_17_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-15_16_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-16_17_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-17_17_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-17_17_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-17_18_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-17_18_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-18_18_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-18_18_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-18_19_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-18_19_null,NEXUS_5,1.0,en].png
diff --git a/tools/localazy/config.json b/tools/localazy/config.json
index d7de5c9eae..4fa301b914 100644
--- a/tools/localazy/config.json
+++ b/tools/localazy/config.json
@@ -53,7 +53,8 @@
{
"name": ":libraries:textcomposer:impl",
"includeRegex": [
- "rich_text_editor.*"
+ "rich_text_editor.*",
+ ".*voice_message_tooltip"
]
},
{