diff --git a/changelog.d/5378.misc b/changelog.d/5378.misc new file mode 100644 index 0000000000..1cf6da5e59 --- /dev/null +++ b/changelog.d/5378.misc @@ -0,0 +1 @@ +Add analytics support for threads \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 01686ca04b..1b01efc074 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.room.timeline import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType @@ -159,6 +160,13 @@ fun TimelineEvent.isSticker(): Boolean { return root.isSticker() } +/** + * Returns whether or not the event is a root thread event + */ +fun TimelineEvent.isRootThread(): Boolean { + return root.threadDetails?.isRootThread.orFalse() +} + /** * Get the latest message body, after a possible edition, stripping the reply prefix if necessary */ diff --git a/vector/src/main/java/im/vector/app/features/analytics/extensions/ComposerExt.kt b/vector/src/main/java/im/vector/app/features/analytics/extensions/ComposerExt.kt new file mode 100644 index 0000000000..407fca2d5b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/extensions/ComposerExt.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 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 im.vector.app.features.analytics.extensions + +import im.vector.app.features.analytics.plan.Composer +import im.vector.app.features.home.room.detail.composer.MessageComposerViewState +import im.vector.app.features.home.room.detail.composer.SendMode + +fun MessageComposerViewState.toAnalyticsComposer(): Composer = + Composer( + inThread = isInThreadTimeline(), + isEditing = sendMode is SendMode.Edit, + isReply = sendMode is SendMode.Reply, + startsThread = startsThread + ) diff --git a/vector/src/main/java/im/vector/app/features/analytics/extensions/InteractionExt.kt b/vector/src/main/java/im/vector/app/features/analytics/extensions/InteractionExt.kt new file mode 100644 index 0000000000..3ed9c419ee --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/extensions/InteractionExt.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 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 im.vector.app.features.analytics.extensions + +import im.vector.app.features.analytics.plan.Interaction + +fun Interaction.Name.toAnalyticsInteraction(interactionType: Interaction.InteractionType = Interaction.InteractionType.Touch) = + Interaction( + name = this, + interactionType = interactionType + ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 86ebe1ec19..ab64f40159 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -118,7 +118,8 @@ import im.vector.app.core.utils.startInstallFromSourceIntent import im.vector.app.core.utils.toast import im.vector.app.databinding.DialogReportContentBinding import im.vector.app.databinding.FragmentTimelineBinding -import im.vector.app.features.analytics.plan.Composer +import im.vector.app.features.analytics.extensions.toAnalyticsInteraction +import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.attachments.AttachmentTypeSelectorView import im.vector.app.features.attachments.AttachmentsHelper @@ -1491,9 +1492,6 @@ class TimelineFragment @Inject constructor( return } if (text.isNotBlank()) { - withState(messageComposerViewModel) { state -> - analyticsTracker.capture(Composer(isThreadTimeLine(), isEditing = state.sendMode is SendMode.Edit, isReply = state.sendMode is SendMode.Reply)) - } // We collapse ASAP, if not there will be a slight annoying delay views.composerLayout.collapse(true) lockSendButton = true @@ -2174,7 +2172,7 @@ class TimelineFragment @Inject constructor( } is EventSharedAction.ReplyInThread -> { if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) { - navigateToThreadTimeline(action.eventId) + navigateToThreadTimeline(action.eventId, action.startsThread) } else { requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } @@ -2333,9 +2331,11 @@ class TimelineFragment @Inject constructor( * using the ThreadsActivity */ - private fun navigateToThreadTimeline(rootThreadEventId: String) { + private fun navigateToThreadTimeline(rootThreadEventId: String, startsThread: Boolean = false) { + analyticsTracker.capture(Interaction.Name.MobileRoomThreadSummaryItem.toAnalyticsInteraction()) context?.let { val roomThreadDetailArgs = ThreadTimelineArgs( + startsThread = startsThread, roomId = timelineArgs.roomId, displayName = timelineViewModel.getRoomSummary()?.displayName, avatarUrl = timelineViewModel.getRoomSummary()?.avatarUrl, @@ -2351,6 +2351,7 @@ class TimelineFragment @Inject constructor( */ private fun navigateToThreadList() { + analyticsTracker.capture(Interaction.Name.MobileRoomThreadListButton.toAnalyticsInteraction()) context?.let { val roomThreadDetailArgs = ThreadTimelineArgs( roomId = timelineArgs.roomId, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index f7975c9029..009d898940 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -27,6 +27,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toAnalyticsComposer import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.command.CommandParser @@ -188,6 +189,9 @@ class MessageComposerViewModel @AssistedInject constructor( private fun handleSendMessage(action: MessageComposerAction.SendMessage) { withState { state -> + analyticsTracker.capture(state.toAnalyticsComposer()).also { + setState { copy(startsThread = false) } + } when (state.sendMode) { is SendMode.Regular -> { when (val slashCommandResult = commandParser.parseSlashCommand( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index f90f3975c6..95553eb1cd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.composer import com.airbnb.mvrx.MavericksState import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent /** @@ -62,6 +63,7 @@ data class MessageComposerViewState( val canSendMessage: CanSendStatus = CanSendStatus.Allowed, val isSendButtonVisible: Boolean = false, val rootThreadEventId: String? = null, + val startsThread: Boolean = false, val sendMode: SendMode = SendMode.Regular("", false), val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle ) : MavericksState { @@ -80,6 +82,7 @@ data class MessageComposerViewState( constructor(args: TimelineArgs) : this( roomId = args.roomId, + startsThread = args.threadTimelineArgs?.startsThread.orFalse(), rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId) fun isInThreadTimeline(): Boolean = rootThreadEventId != null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt index 048a4754f5..5f12c2f174 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -48,7 +48,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, data class Reply(val eventId: String) : EventSharedAction(R.string.reply, R.drawable.ic_reply) - data class ReplyInThread(val eventId: String) : + data class ReplyInThread(val eventId: String, val startsThread: Boolean) : EventSharedAction(R.string.reply_in_thread, R.drawable.ic_reply_in_thread) object ViewInRoom : diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index b99dbcc220..bd4e93b25d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -61,6 +61,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited import org.matrix.android.sdk.api.session.room.timeline.isPoll +import org.matrix.android.sdk.api.session.room.timeline.isRootThread import org.matrix.android.sdk.api.session.room.timeline.isSticker import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.unwrap @@ -329,7 +330,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } if (canReplyInThread(timelineEvent, messageContent, actionPermissions)) { - add(EventSharedAction.ReplyInThread(eventId)) + add(EventSharedAction.ReplyInThread(eventId, !timelineEvent.isRootThread())) } if (canViewInRoom(timelineEvent, messageContent, actionPermissions)) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 659cef2b37..3189954e20 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -155,7 +155,7 @@ class MessageItemFactory @Inject constructor( if (event.root.isRedacted()) { // message is redacted - val attributes = messageItemAttributesFactory.create(null, informationData, callback, params.reactionsSummaryEvents) + val attributes = messageItemAttributesFactory.create(null, informationData, callback, params.reactionsSummaryEvents, threadDetails) return buildRedactedItem(attributes, highlight) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt index fc76535c4c..726138ed93 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -26,6 +26,8 @@ import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityThreadsBinding +import im.vector.app.features.analytics.extensions.toAnalyticsInteraction +import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.TimelineFragment import im.vector.app.features.home.room.detail.arguments.TimelineArgs @@ -92,6 +94,7 @@ class ThreadsActivity : VectorBaseActivity() { * One usage of that is from the Threads Activity */ fun navigateToThreadTimeline(threadTimelineArgs: ThreadTimelineArgs) { + analyticsTracker.capture(Interaction.Name.MobileThreadListThreadItem.toAnalyticsInteraction()) val commonOption: (FragmentTransaction) -> Unit = { it.setCustomAnimations( R.anim.animation_slide_in_right, diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt index aadad3d97c..d3a80811ea 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt @@ -26,5 +26,6 @@ data class ThreadTimelineArgs( val displayName: String?, val avatarUrl: String?, val roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, - val rootThreadEventId: String? = null + val rootThreadEventId: String? = null, + val startsThread: Boolean = false ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt index 7a22f75bce..7f18d172e4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt @@ -25,6 +25,9 @@ import dagger.assisted.AssistedInject import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.extensions.toAnalyticsInteraction +import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.home.room.threads.list.views.ThreadListFragment import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -34,6 +37,7 @@ import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent import org.matrix.android.sdk.flow.flow class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState: ThreadListViewState, + private val analyticsTracker: AnalyticsTracker, private val session: Session) : VectorViewModel(initialState) { @@ -113,9 +117,10 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState } } - fun canHomeserverUseThreading() = session.getHomeServerCapabilities().canUseThreading + fun canHomeserverUseThreading() = session.getHomeServerCapabilities().canUseThreading fun applyFiltering(shouldFilterThreads: Boolean) { + analyticsTracker.capture(Interaction.Name.MobileThreadListFilterItem.toAnalyticsInteraction()) setState { copy(shouldFilterThreads = shouldFilterThreads) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt index 949778629b..d5659efa49 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -30,6 +30,7 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentThreadListBinding +import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.animation.TimelineItemAnimator import im.vector.app.features.home.room.threads.ThreadsActivity @@ -62,6 +63,7 @@ class ThreadListFragment @Inject constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + analyticsScreenName = MobileScreen.ScreenName.ThreadList } override fun onOptionsItemSelected(item: MenuItem): Boolean {