Add analytics for voice messages (#1706)
This commit is contained in:
parent
4e7750b70a
commit
dab5e0d0ca
8 changed files with 76 additions and 10 deletions
|
|
@ -28,7 +28,7 @@ import io.element.android.features.location.impl.common.permissions.PermissionsE
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
|
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||||
import io.element.android.features.messages.test.MessageComposerContextFake
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
|
@ -49,7 +49,7 @@ class SendLocationPresenterTest {
|
||||||
private val permissionsPresenterFake = PermissionsPresenterFake()
|
private val permissionsPresenterFake = PermissionsPresenterFake()
|
||||||
private val fakeMatrixRoom = FakeMatrixRoom()
|
private val fakeMatrixRoom = FakeMatrixRoom()
|
||||||
private val fakeAnalyticsService = FakeAnalyticsService()
|
private val fakeAnalyticsService = FakeAnalyticsService()
|
||||||
private val messageComposerContextFake = MessageComposerContextFake()
|
private val fakeMessageComposerContext = FakeMessageComposerContext()
|
||||||
private val fakeLocationActions = FakeLocationActions()
|
private val fakeLocationActions = FakeLocationActions()
|
||||||
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
||||||
private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter(
|
private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter(
|
||||||
|
|
@ -58,7 +58,7 @@ class SendLocationPresenterTest {
|
||||||
},
|
},
|
||||||
room = fakeMatrixRoom,
|
room = fakeMatrixRoom,
|
||||||
analyticsService = fakeAnalyticsService,
|
analyticsService = fakeAnalyticsService,
|
||||||
messageComposerContext = messageComposerContextFake,
|
messageComposerContext = fakeMessageComposerContext,
|
||||||
locationActions = fakeLocationActions,
|
locationActions = fakeLocationActions,
|
||||||
buildMeta = fakeBuildMeta,
|
buildMeta = fakeBuildMeta,
|
||||||
)
|
)
|
||||||
|
|
@ -379,7 +379,7 @@ class SendLocationPresenterTest {
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
messageComposerContextFake.apply {
|
fakeMessageComposerContext.apply {
|
||||||
composerMode = MessageComposerMode.Edit(
|
composerMode = MessageComposerMode.Edit(
|
||||||
eventId = null, defaultContent = "", transactionId = null
|
eventId = null, defaultContent = "", transactionId = null
|
||||||
)
|
)
|
||||||
|
|
@ -425,7 +425,7 @@ class SendLocationPresenterTest {
|
||||||
shouldShowRationale = false,
|
shouldShowRationale = false,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
messageComposerContextFake.apply {
|
fakeMessageComposerContext.apply {
|
||||||
composerMode = MessageComposerMode.Edit(
|
composerMode = MessageComposerMode.Edit(
|
||||||
eventId = null, defaultContent = "", transactionId = null
|
eventId = null, defaultContent = "", transactionId = null
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.dateformatter.test)
|
testImplementation(projects.libraries.dateformatter.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
|
testImplementation(projects.features.messages.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
|
import io.element.android.features.messages.api.MessageComposerContext
|
||||||
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
|
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
|
@ -56,6 +58,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val mediaSender: MediaSender,
|
private val mediaSender: MediaSender,
|
||||||
private val player: VoiceMessageComposerPlayer,
|
private val player: VoiceMessageComposerPlayer,
|
||||||
|
private val messageComposerContext: MessageComposerContext,
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory
|
permissionsPresenterFactory: PermissionsPresenter.Factory
|
||||||
) : Presenter<VoiceMessageComposerState> {
|
) : Presenter<VoiceMessageComposerState> {
|
||||||
private val permissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.RECORD_AUDIO)
|
private val permissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.RECORD_AUDIO)
|
||||||
|
|
@ -151,6 +154,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
isSending = true
|
isSending = true
|
||||||
player.pause()
|
player.pause()
|
||||||
|
analyticsService.captureComposerEvent()
|
||||||
appCoroutineScope.sendMessage(
|
appCoroutineScope.sendMessage(
|
||||||
file = finishedState.file,
|
file = finishedState.file,
|
||||||
mimeType = finishedState.mimeType,
|
mimeType = finishedState.mimeType,
|
||||||
|
|
@ -236,6 +240,16 @@ class VoiceMessageComposerPresenter @Inject constructor(
|
||||||
|
|
||||||
voiceRecorder.deleteRecording()
|
voiceRecorder.deleteRecording()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun AnalyticsService.captureComposerEvent() =
|
||||||
|
analyticsService.capture(
|
||||||
|
Composer(
|
||||||
|
inThread = messageComposerContext.composerMode.inThread,
|
||||||
|
isEditing = messageComposerContext.composerMode.isEditing,
|
||||||
|
isReply = messageComposerContext.composerMode.isReply,
|
||||||
|
messageType = Composer.MessageType.VoiceMessage,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun VoiceRecorderState.finishedWaveform(): ImmutableList<Float> =
|
private fun VoiceRecorderState.finishedWaveform(): ImmutableList<Float> =
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
||||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||||
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
import io.element.android.features.messages.textcomposer.TestRichTextEditorStateFactory
|
import io.element.android.features.messages.textcomposer.TestRichTextEditorStateFactory
|
||||||
import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider
|
import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider
|
||||||
import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter
|
import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter
|
||||||
|
|
@ -641,6 +642,7 @@ class MessagesPresenterTest {
|
||||||
analyticsService,
|
analyticsService,
|
||||||
mediaSender,
|
mediaSender,
|
||||||
player = VoiceMessageComposerPlayer(FakeMediaPlayer()),
|
player = VoiceMessageComposerPlayer(FakeMediaPlayer()),
|
||||||
|
messageComposerContext = FakeMessageComposerContext(),
|
||||||
permissionsPresenterFactory,
|
permissionsPresenterFactory,
|
||||||
)
|
)
|
||||||
val timelinePresenter = TimelinePresenter(
|
val timelinePresenter = TimelinePresenter(
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,16 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.TurbineTestContext
|
import app.cash.turbine.TurbineTestContext
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
|
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
||||||
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
|
@ -38,6 +43,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.PressEvent
|
import io.element.android.libraries.textcomposer.model.PressEvent
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
||||||
|
|
@ -65,6 +71,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
private val matrixRoom = FakeMatrixRoom()
|
private val matrixRoom = FakeMatrixRoom()
|
||||||
private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() }
|
private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() }
|
||||||
private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom)
|
private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom)
|
||||||
|
private val messageComposerContext = FakeMessageComposerContext()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val RECORDING_DURATION = 1.seconds
|
private val RECORDING_DURATION = 1.seconds
|
||||||
|
|
@ -275,6 +282,35 @@ class VoiceMessageComposerPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - sending is tracked`() = runTest {
|
||||||
|
val presenter = createVoiceMessageComposerPresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
// Send a normal voice message
|
||||||
|
messageComposerContext.composerMode = MessageComposerMode.Normal
|
||||||
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
|
||||||
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
|
||||||
|
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||||
|
skipItems(1) // Sending state
|
||||||
|
|
||||||
|
// Now reply with a voice message
|
||||||
|
messageComposerContext.composerMode = aReplyMode()
|
||||||
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
|
||||||
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
|
||||||
|
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||||
|
val finalState = awaitItem() // Sending state
|
||||||
|
|
||||||
|
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||||
|
aVoiceMessageComposerEvent(isReply = false),
|
||||||
|
aVoiceMessageComposerEvent(isReply = true)
|
||||||
|
)
|
||||||
|
|
||||||
|
testPauseAndDestroy(finalState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - send while playing`() = runTest {
|
fun `present - send while playing`() = runTest {
|
||||||
val presenter = createVoiceMessageComposerPresenter()
|
val presenter = createVoiceMessageComposerPresenter()
|
||||||
|
|
@ -565,6 +601,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
analyticsService,
|
analyticsService,
|
||||||
mediaSender,
|
mediaSender,
|
||||||
player = VoiceMessageComposerPlayer(FakeMediaPlayer()),
|
player = VoiceMessageComposerPlayer(FakeMediaPlayer()),
|
||||||
|
messageComposerContext = messageComposerContext,
|
||||||
FakePermissionsPresenterFactory(permissionsPresenter),
|
FakePermissionsPresenterFactory(permissionsPresenter),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -595,3 +632,15 @@ class VoiceMessageComposerPresenterTest {
|
||||||
waveform = waveform.toImmutableList(),
|
waveform = waveform.toImmutableList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, false, AN_EVENT_ID, A_MESSAGE)
|
||||||
|
|
||||||
|
private fun aVoiceMessageComposerEvent(
|
||||||
|
isReply: Boolean = false
|
||||||
|
) = Composer(
|
||||||
|
inThread = false,
|
||||||
|
isEditing = false,
|
||||||
|
isReply = isReply,
|
||||||
|
messageType = Composer.MessageType.VoiceMessage,
|
||||||
|
startsThread = null
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,6 @@ package io.element.android.features.messages.test
|
||||||
import io.element.android.features.messages.api.MessageComposerContext
|
import io.element.android.features.messages.api.MessageComposerContext
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
|
|
||||||
class MessageComposerContextFake(
|
class FakeMessageComposerContext(
|
||||||
override var composerMode: MessageComposerMode = MessageComposerMode.Normal
|
override var composerMode: MessageComposerMode = MessageComposerMode.Normal
|
||||||
) : MessageComposerContext
|
) : MessageComposerContext
|
||||||
|
|
@ -22,7 +22,7 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import im.vector.app.features.analytics.plan.PollCreation
|
import im.vector.app.features.analytics.plan.PollCreation
|
||||||
import io.element.android.features.messages.test.MessageComposerContextFake
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.test.room.CreatePollInvocation
|
import io.element.android.libraries.matrix.test.room.CreatePollInvocation
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
|
@ -41,12 +41,12 @@ class CreatePollPresenterTest {
|
||||||
private var navUpInvocationsCount = 0
|
private var navUpInvocationsCount = 0
|
||||||
private val fakeMatrixRoom = FakeMatrixRoom()
|
private val fakeMatrixRoom = FakeMatrixRoom()
|
||||||
private val fakeAnalyticsService = FakeAnalyticsService()
|
private val fakeAnalyticsService = FakeAnalyticsService()
|
||||||
private val messageComposerContextFake = MessageComposerContextFake()
|
private val fakeMessageComposerContext = FakeMessageComposerContext()
|
||||||
|
|
||||||
private val presenter = CreatePollPresenter(
|
private val presenter = CreatePollPresenter(
|
||||||
room = fakeMatrixRoom,
|
room = fakeMatrixRoom,
|
||||||
analyticsService = fakeAnalyticsService,
|
analyticsService = fakeAnalyticsService,
|
||||||
messageComposerContext = messageComposerContextFake,
|
messageComposerContext = fakeMessageComposerContext,
|
||||||
navigateUp = { navUpInvocationsCount++ },
|
navigateUp = { navUpInvocationsCount++ },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ opusencoder = "io.element.android:opusencoder:1.1.0"
|
||||||
# Analytics
|
# Analytics
|
||||||
posthog = "com.posthog.android:posthog:2.0.3"
|
posthog = "com.posthog.android:posthog:2.0.3"
|
||||||
sentry = "io.sentry:sentry-android:6.32.0"
|
sentry = "io.sentry:sentry-android:6.32.0"
|
||||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:e9cd9adaf18cec52ed851395eb84358b4f9b8d7f"
|
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:aa14cbcdf81af2746d20a71779ec751f971e1d7f"
|
||||||
|
|
||||||
# Emojibase
|
# Emojibase
|
||||||
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3"
|
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue