Merge pull request #5692 from vector-im/feature/aris/threads_beta_infrom_users_on_reply

Threads Beta opt-in mechanism
This commit is contained in:
Benoit Marty 2022-04-05 20:40:16 +02:00 committed by GitHub
commit 0f14652932
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 146 additions and 6 deletions

1
changelog.d/5692.misc Normal file
View File

@ -0,0 +1 @@
Implement threads beta opt-in mechanism to notify users about threads

View File

@ -45,9 +45,11 @@ internal class RealmSendingEventsDataSource(
private var frozenSendingTimelineEvents: RealmList<TimelineEventEntity>? = null private var frozenSendingTimelineEvents: RealmList<TimelineEventEntity>? = null
private val sendingTimelineEventsListener = RealmChangeListener<RealmList<TimelineEventEntity>> { events -> private val sendingTimelineEventsListener = RealmChangeListener<RealmList<TimelineEventEntity>> { events ->
uiEchoManager.onSentEventsInDatabase(events.map { it.eventId }) if (events.isValid) {
updateFrozenResults(events) uiEchoManager.onSentEventsInDatabase(events.map { it.eventId })
onEventsUpdated(false) updateFrozenResults(events)
onEventsUpdated(false)
}
} }
override fun start() { override fun start() {

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- This file contains url values-->
<string name="threads_learn_more_url" translatable="false">https://element.io/help#threads</string>
</resources>

View File

@ -71,6 +71,9 @@ abstract class BottomSheetActionItem : VectorEpoxyModel<BottomSheetActionItem.Ho
@EpoxyAttribute @EpoxyAttribute
var destructive = false var destructive = false
@EpoxyAttribute
var showBetaLabel = false
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
lateinit var listener: ClickListener lateinit var listener: ClickListener
@ -106,6 +109,7 @@ abstract class BottomSheetActionItem : VectorEpoxyModel<BottomSheetActionItem.Ho
} else { } else {
holder.text.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null) holder.text.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
} }
holder.betaLabel.isVisible = showBetaLabel
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
@ -113,5 +117,6 @@ abstract class BottomSheetActionItem : VectorEpoxyModel<BottomSheetActionItem.Ho
val icon by bind<ImageView>(R.id.actionIcon) val icon by bind<ImageView>(R.id.actionIcon)
val text by bind<TextView>(R.id.actionTitle) val text by bind<TextView>(R.id.actionTitle)
val selected by bind<ImageView>(R.id.actionSelected) val selected by bind<ImageView>(R.id.actionSelected)
val betaLabel by bind<TextView>(R.id.actionBetaTextView)
} }
} }

View File

@ -25,6 +25,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Spannable import android.text.Spannable
import android.text.format.DateUtils import android.text.format.DateUtils
import android.text.method.LinkMovementMethod
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
@ -170,6 +171,7 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
import im.vector.app.features.home.room.detail.views.RoomDetailLazyLoadedViews import im.vector.app.features.home.room.detail.views.RoomDetailLazyLoadedViews
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
import im.vector.app.features.home.room.threads.ThreadsManager
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillImageSpan import im.vector.app.features.html.PillImageSpan
@ -252,6 +254,7 @@ class TimelineFragment @Inject constructor(
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
private val eventHtmlRenderer: EventHtmlRenderer, private val eventHtmlRenderer: EventHtmlRenderer,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val threadsManager: ThreadsManager,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter, private val dimensionConverter: DimensionConverter,
private val userPreferencesProvider: UserPreferencesProvider, private val userPreferencesProvider: UserPreferencesProvider,
@ -2213,7 +2216,7 @@ class TimelineFragment @Inject constructor(
} }
is EventSharedAction.ReplyInThread -> { is EventSharedAction.ReplyInThread -> {
if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) { if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) {
navigateToThreadTimeline(action.eventId, action.startsThread) onReplyInThreadClicked(action)
} else { } else {
requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit)
} }
@ -2369,6 +2372,14 @@ class TimelineFragment @Inject constructor(
.show() .show()
} }
private fun onReplyInThreadClicked(action: EventSharedAction.ReplyInThread) {
if (vectorPreferences.areThreadMessagesEnabled()) {
navigateToThreadTimeline(action.eventId, action.startsThread)
} else {
displayThreadsBetaOptInDialog()
}
}
/** /**
* Navigate to Threads timeline for the specified rootThreadEventId * Navigate to Threads timeline for the specified rootThreadEventId
* using the ThreadsActivity * using the ThreadsActivity
@ -2388,6 +2399,25 @@ class TimelineFragment @Inject constructor(
} }
} }
private fun displayThreadsBetaOptInDialog() {
activity?.let {
MaterialAlertDialogBuilder(it)
.setTitle(R.string.threads_beta_enable_notice_title)
.setMessage(threadsManager.getBetaEnableThreadsMessage())
.setCancelable(true)
.setNegativeButton(R.string.action_not_now) { _, _ -> }
.setPositiveButton(R.string.action_try_it_out) { _, _ ->
threadsManager.enableThreadsAndRestart(it)
}
.show()
?.findViewById<TextView>(android.R.id.message)
?.apply {
linksClickable = true
movementMethod = LinkMovementMethod.getInstance()
}
}
}
/** /**
* Navigate to Threads list for the current room * Navigate to Threads list for the current room
* using the ThreadsActivity * using the ThreadsActivity

View File

@ -43,6 +43,7 @@ import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE
import im.vector.app.features.location.UrlMapProvider import im.vector.app.features.location.UrlMapProvider
import im.vector.app.features.location.toLocationData import im.vector.app.features.location.toLocationData
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.settings.VectorPreferences
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.extensions.orTrue
@ -64,6 +65,7 @@ class MessageActionsEpoxyController @Inject constructor(
private val errorFormatter: ErrorFormatter, private val errorFormatter: ErrorFormatter,
private val spanUtils: SpanUtils, private val spanUtils: SpanUtils,
private val eventDetailsFormatter: EventDetailsFormatter, private val eventDetailsFormatter: EventDetailsFormatter,
private val vectorPreferences: VectorPreferences,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val urlMapProvider: UrlMapProvider, private val urlMapProvider: UrlMapProvider,
private val locationPinProvider: LocationPinProvider private val locationPinProvider: LocationPinProvider
@ -187,6 +189,8 @@ class MessageActionsEpoxyController @Inject constructor(
id("separator_$index") id("separator_$index")
} }
} else { } else {
val showBetaLabel = action.shouldShowBetaLabel()
bottomSheetActionItem { bottomSheetActionItem {
id("action_$index") id("action_$index")
iconRes(action.iconResId) iconRes(action.iconResId)
@ -195,6 +199,7 @@ class MessageActionsEpoxyController @Inject constructor(
expanded(state.expendedReportContentMenu) expanded(state.expendedReportContentMenu)
listener { host.listener?.didSelectMenuAction(action) } listener { host.listener?.didSelectMenuAction(action) }
destructive(action.destructive) destructive(action.destructive)
showBetaLabel(showBetaLabel)
} }
if (action is EventSharedAction.ReportContent && state.expendedReportContentMenu) { if (action is EventSharedAction.ReportContent && state.expendedReportContentMenu) {
@ -217,6 +222,9 @@ class MessageActionsEpoxyController @Inject constructor(
} }
} }
private fun EventSharedAction.shouldShowBetaLabel(): Boolean =
this is EventSharedAction.ReplyInThread && !vectorPreferences.areThreadMessagesEnabled()
interface MessageActionsEpoxyControllerListener : TimelineEventController.UrlClickCallback { interface MessageActionsEpoxyControllerListener : TimelineEventController.UrlClickCallback {
fun didSelectMenuAction(eventAction: EventSharedAction) fun didSelectMenuAction(eventAction: EventSharedAction)
} }

View File

@ -450,7 +450,8 @@ class MessageActionsViewModel @AssistedInject constructor(
private fun canReplyInThread(event: TimelineEvent, private fun canReplyInThread(event: TimelineEvent,
messageContent: MessageContent?, messageContent: MessageContent?,
actionPermissions: ActionPermissions): Boolean { actionPermissions: ActionPermissions): Boolean {
if (!vectorPreferences.areThreadMessagesEnabled()) return false // We let reply in thread visible even if threads are not enabled, with an enhanced flow to attract users
// if (!vectorPreferences.areThreadMessagesEnabled()) return false
if (initialState.isFromThreadTimeline) return false if (initialState.isFromThreadTimeline) return false
if (event.root.isThread()) return false if (event.root.isThread()) return false
if (event.root.getClearType() != EventType.MESSAGE && if (event.root.getClearType() != EventType.MESSAGE &&

View File

@ -0,0 +1,59 @@
/*
* 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.home.room.threads
import android.app.Activity
import android.text.Spanned
import androidx.core.text.HtmlCompat
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
import javax.inject.Inject
/**
* The class is responsible for handling thread specific tasks
*/
class ThreadsManager @Inject constructor(
private val vectorPreferences: VectorPreferences,
private val lightweightSettingsStorage: LightweightSettingsStorage,
private val stringProvider: StringProvider
) {
/**
* Enable threads and invoke an initial sync. The initial sync is mandatory in order to change
* the already saved DB schema for already received messages
*/
fun enableThreadsAndRestart(activity: Activity) {
vectorPreferences.setThreadMessagesEnabled()
lightweightSettingsStorage.setThreadMessagesEnabled(vectorPreferences.areThreadMessagesEnabled())
MainActivity.restartApp(activity, MainActivityArgs(clearCache = true))
}
/**
* Generates and return an Html spanned string to be rendered especially in dialogs
*/
fun getBetaEnableThreadsMessage(): Spanned {
val learnMore = stringProvider.getString(R.string.action_learn_more)
val learnMoreUrl = stringProvider.getString(R.string.threads_learn_more_url)
val href = "<a href='$learnMoreUrl'>$learnMore</a>.<br><br>"
val message = stringProvider.getString(R.string.threads_beta_enable_notice_message, href)
return HtmlCompat.fromHtml(message, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
}

View File

@ -29,7 +29,6 @@ import javax.inject.Inject
class VectorSettingsLabsFragment @Inject constructor( class VectorSettingsLabsFragment @Inject constructor(
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val lightweightSettingsStorage: LightweightSettingsStorage private val lightweightSettingsStorage: LightweightSettingsStorage
) : VectorSettingsBaseFragment() { ) : VectorSettingsBaseFragment() {
override var titleRes = R.string.room_settings_labs_pref_title override var titleRes = R.string.room_settings_labs_pref_title

View File

@ -82,4 +82,27 @@
tools:ignore="MissingPrefix" tools:ignore="MissingPrefix"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView
android:id="@+id/actionBetaTextView"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/notification_badge"
android:backgroundTint="@color/palette_azure"
android:gravity="center"
android:paddingStart="10dp"
android:paddingTop="3dp"
android:paddingEnd="10dp"
android:layout_marginEnd="1dp"
android:paddingBottom="3dp"
android:visibility="gone"
tools:visibility="visible"
android:text="@string/beta_title_bottom_sheet_action"
android:textColor="@color/palette_white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/actionSelected"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -360,6 +360,7 @@
<string name="action_enable">Enable</string> <string name="action_enable">Enable</string>
<string name="action_disable">Disable</string> <string name="action_disable">Disable</string>
<string name="action_not_now">Not now</string> <string name="action_not_now">Not now</string>
<string name="action_try_it_out">Try it out</string>
<string name="action_agree">Agree</string> <string name="action_agree">Agree</string>
<string name="action_change">"Change"</string> <string name="action_change">"Change"</string>
<string name="action_remove">Remove</string> <string name="action_remove">Remove</string>
@ -384,6 +385,7 @@
<string name="action_play">Play</string> <string name="action_play">Play</string>
<string name="action_dismiss">Dismiss</string> <string name="action_dismiss">Dismiss</string>
<string name="action_reset">Reset</string> <string name="action_reset">Reset</string>
<string name="action_learn_more">Learn more</string>
<string name="copied_to_clipboard">Copied to clipboard</string> <string name="copied_to_clipboard">Copied to clipboard</string>
@ -734,6 +736,9 @@
<string name="search_thread_from_a_thread">From a Thread</string> <string name="search_thread_from_a_thread">From a Thread</string>
<string name="threads_notice_migration_title">Threads Approaching Beta 🎉</string> <string name="threads_notice_migration_title">Threads Approaching Beta 🎉</string>
<string name="threads_notice_migration_message">Were getting closer to releasing a public Beta for Threads.\n\nAs we prepare for it, we need to make some changes: threads created before this point will be displayed as regular replies.\n\nThis will be a one-off transition as Threads are now part of the Matrix specification.</string> <string name="threads_notice_migration_message">Were getting closer to releasing a public Beta for Threads.\n\nAs we prepare for it, we need to make some changes: threads created before this point will be displayed as regular replies.\n\nThis will be a one-off transition as Threads are now part of the Matrix specification.</string>
<string name="threads_beta_enable_notice_title">Threads Beta</string>
<!-- %s will be replaced with action_learn_more string resource that will be clickable(url redirection) -->
<string name="threads_beta_enable_notice_message">Threads help keep your conversations on-topic and easy to track. %sEnabling threads will refresh the app. This may take longer for some accounts.</string>
<!-- Search --> <!-- Search -->
<string name="search_hint">Search</string> <string name="search_hint">Search</string>
@ -1643,6 +1648,7 @@
<string name="send_suggestion_sent">Thanks, the suggestion has been successfully sent</string> <string name="send_suggestion_sent">Thanks, the suggestion has been successfully sent</string>
<string name="send_suggestion_failed">The suggestion failed to be sent (%s)</string> <string name="send_suggestion_failed">The suggestion failed to be sent (%s)</string>
<string name="beta_title_bottom_sheet_action">BETA</string>
<string name="send_feedback_space_title">Spaces feedback</string> <string name="send_feedback_space_title">Spaces feedback</string>
<string name="feedback">Feedback</string> <string name="feedback">Feedback</string>
<string name="send_feedback_space_info">Youre using a beta version of spaces. Your feedback will help inform the next versions. Your platform and username will be noted to help us use your feedback as much as we can.</string> <string name="send_feedback_space_info">Youre using a beta version of spaces. Your feedback will help inform the next versions. Your platform and username will be noted to help us use your feedback as much as we can.</string>