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..6c30ae6f63 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 81b037e016..4fbe32bc48 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 @@ -169,6 +170,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 @@ -251,6 +253,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, @@ -2199,7 +2202,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) } @@ -2355,6 +2358,14 @@ class TimelineFragment @Inject constructor( .show() } + private fun onReplyInThreadClicked(action: EventSharedAction.ReplyInThread) { + if (vectorPreferences.areThreadMessagesEnabled()) { + navigateToThreadTimeline(action.eventId, action.startsThread) + } else { + displayThreadsBetaNotice() + } + } + /** * Navigate to Threads timeline for the specified rootThreadEventId * using the ThreadsActivity @@ -2374,6 +2385,29 @@ class TimelineFragment @Inject constructor( } } + /** + * Display a dialog that will let the user to enable threads + */ + + private fun displayThreadsBetaNotice() = + 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) { _, _ -> + timelineViewModel.onCleared() // We should first clear our viewModel + 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..8758bd1efd 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..1e7ef672f3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsManager.kt @@ -0,0 +1,61 @@ +/* + * 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.content.Context +import android.text.Spanned +import androidx.core.text.HtmlCompat +import im.vector.app.R +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 context: Context +) { + + companion object { + const val learnMoreUrl = "https://element.io/help#threads" + } + + /** + * 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 href = "Learn more.

" + val message = context.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 003832fb97..605a42ff02 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 @@ -27,7 +27,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 9bdbb291a5..03f3ef93f1 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 @@ -734,6 +735,8 @@ 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 +1646,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.