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:
commit
0f14652932
|
@ -0,0 +1 @@
|
||||||
|
Implement threads beta opt-in mechanism to notify users about threads
|
|
@ -45,10 +45,12 @@ 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 ->
|
||||||
|
if (events.isValid) {
|
||||||
uiEchoManager.onSentEventsInDatabase(events.map { it.eventId })
|
uiEchoManager.onSentEventsInDatabase(events.map { it.eventId })
|
||||||
updateFrozenResults(events)
|
updateFrozenResults(events)
|
||||||
onEventsUpdated(false)
|
onEventsUpdated(false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
val safeRealm = realm.get()
|
val safeRealm = realm.get()
|
||||||
|
|
|
@ -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>
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 &&
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">We’re 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">We’re 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">You’re 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">You’re 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>
|
||||||
|
|
Loading…
Reference in New Issue