diff --git a/changelog.d/5692.misc b/changelog.d/5692.misc new file mode 100644 index 0000000000..66f1d35eb0 --- /dev/null +++ b/changelog.d/5692.misc @@ -0,0 +1 @@ +Implement threads beta opt-in mechanism to notify users about threads \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt index cb61222de7..637267a9b1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt @@ -45,9 +45,11 @@ internal class RealmSendingEventsDataSource( private var frozenSendingTimelineEvents: RealmList? = null private val sendingTimelineEventsListener = RealmChangeListener> { events -> - uiEchoManager.onSentEventsInDatabase(events.map { it.eventId }) - updateFrozenResults(events) - onEventsUpdated(false) + if (events.isValid) { + uiEchoManager.onSentEventsInDatabase(events.map { it.eventId }) + updateFrozenResults(events) + onEventsUpdated(false) + } } override fun start() { diff --git a/vector-config/src/main/res/values/urls.xml b/vector-config/src/main/res/values/urls.xml new file mode 100644 index 0000000000..22e3a9ac72 --- /dev/null +++ b/vector-config/src/main/res/values/urls.xml @@ -0,0 +1,6 @@ + + + + + https://element.io/help#threads + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt index 854a5d3419..ed3d55fca9 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt @@ -71,6 +71,9 @@ abstract class BottomSheetActionItem : VectorEpoxyModel(R.id.actionIcon) val text by bind(R.id.actionTitle) val selected by bind(R.id.actionSelected) + val betaLabel by bind(R.id.actionBetaTextView) } } 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 ffb6ab6d26..a903b87669 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 @@ -25,6 +25,7 @@ import android.os.Build import android.os.Bundle import android.text.Spannable import android.text.format.DateUtils +import android.text.method.LinkMovementMethod import android.view.HapticFeedbackConstants import android.view.KeyEvent 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.views.RoomDetailLazyLoadedViews 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.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan @@ -252,6 +254,7 @@ class TimelineFragment @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val eventHtmlRenderer: EventHtmlRenderer, private val vectorPreferences: VectorPreferences, + private val threadsManager: ThreadsManager, private val colorProvider: ColorProvider, private val dimensionConverter: DimensionConverter, private val userPreferencesProvider: UserPreferencesProvider, @@ -2213,7 +2216,7 @@ class TimelineFragment @Inject constructor( } is EventSharedAction.ReplyInThread -> { if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) { - navigateToThreadTimeline(action.eventId, action.startsThread) + onReplyInThreadClicked(action) } else { requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } @@ -2369,6 +2372,14 @@ class TimelineFragment @Inject constructor( .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 * 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(android.R.id.message) + ?.apply { + linksClickable = true + movementMethod = LinkMovementMethod.getInstance() + } + } + } + /** * Navigate to Threads list for the current room * using the ThreadsActivity diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 27937047a5..307be220d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -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.toLocationData import im.vector.app.features.media.ImageContentRenderer +import im.vector.app.features.settings.VectorPreferences import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orTrue @@ -64,6 +65,7 @@ class MessageActionsEpoxyController @Inject constructor( private val errorFormatter: ErrorFormatter, private val spanUtils: SpanUtils, private val eventDetailsFormatter: EventDetailsFormatter, + private val vectorPreferences: VectorPreferences, private val dateFormatter: VectorDateFormatter, private val urlMapProvider: UrlMapProvider, private val locationPinProvider: LocationPinProvider @@ -187,6 +189,8 @@ class MessageActionsEpoxyController @Inject constructor( id("separator_$index") } } else { + val showBetaLabel = action.shouldShowBetaLabel() + bottomSheetActionItem { id("action_$index") iconRes(action.iconResId) @@ -195,6 +199,7 @@ class MessageActionsEpoxyController @Inject constructor( expanded(state.expendedReportContentMenu) listener { host.listener?.didSelectMenuAction(action) } destructive(action.destructive) + showBetaLabel(showBetaLabel) } 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 { fun didSelectMenuAction(eventAction: EventSharedAction) } 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 aaaecb0a13..9a73afd897 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 @@ -450,7 +450,8 @@ class MessageActionsViewModel @AssistedInject constructor( private fun canReplyInThread(event: TimelineEvent, messageContent: MessageContent?, 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 (event.root.isThread()) return false if (event.root.getClearType() != EventType.MESSAGE && diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsManager.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsManager.kt new file mode 100644 index 0000000000..469a12019a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsManager.kt @@ -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 = "$learnMore.

" + val message = stringProvider.getString(R.string.threads_beta_enable_notice_message, href) + return HtmlCompat.fromHtml(message, HtmlCompat.FROM_HTML_MODE_LEGACY) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt index fb2fb2b490..50ef864dbf 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt @@ -29,7 +29,6 @@ import javax.inject.Inject class VectorSettingsLabsFragment @Inject constructor( private val vectorPreferences: VectorPreferences, private val lightweightSettingsStorage: LightweightSettingsStorage - ) : VectorSettingsBaseFragment() { override var titleRes = R.string.room_settings_labs_pref_title diff --git a/vector/src/main/res/layout/item_bottom_sheet_action.xml b/vector/src/main/res/layout/item_bottom_sheet_action.xml index bffcb79df6..e5230620d4 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_action.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_action.xml @@ -82,4 +82,27 @@ tools:ignore="MissingPrefix" tools:visibility="visible" /> + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index e61d3f9753..0ab450421c 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -360,6 +360,7 @@ Enable Disable Not now + Try it out Agree "Change" Remove @@ -384,6 +385,7 @@ Play Dismiss Reset + Learn more Copied to clipboard @@ -734,6 +736,9 @@ From a Thread Threads Approaching Beta 🎉 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. + Threads Beta + + Threads help keep your conversations on-topic and easy to track. %sEnabling threads will refresh the app. This may take longer for some accounts. Search @@ -1643,6 +1648,7 @@ Thanks, the suggestion has been successfully sent The suggestion failed to be sent (%s) + BETA Spaces feedback Feedback 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.