diff --git a/vector/src/debug/res/layout/fragment_thread_list.xml b/vector/src/debug/res/layout/fragment_thread_list.xml new file mode 100644 index 0000000000..cf3a79e776 --- /dev/null +++ b/vector/src/debug/res/layout/fragment_thread_list.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 2b7b445ad5..f0e68e8446 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -179,8 +179,7 @@ - - + (), CallContro private fun returnToChat() { val roomId = withState(callViewModel) { it.roomId } - val args = RoomDetailArgs(roomId) + val args = TimelineArgs(roomId) val intent = RoomDetailActivity.newIntent(this, args).apply { flags = FLAG_ACTIVITY_CLEAR_TOP } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index 6c009d3786..ea0fb3f0ca 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailActivity -import im.vector.app.features.home.room.detail.RoomDetailArgs +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.VerificationVectorAlert import org.matrix.android.sdk.api.session.Session @@ -142,7 +142,7 @@ class IncomingVerificationRequestHandler @Inject constructor( R.drawable.ic_shield_black, shouldBeDisplayedIn = { activity -> if (activity is RoomDetailActivity) { - activity.intent?.extras?.getParcelable(RoomDetailActivity.EXTRA_ROOM_DETAIL_ARGS)?.let { + activity.intent?.extras?.getParcelable(RoomDetailActivity.EXTRA_ROOM_DETAIL_ARGS)?.let { it.roomId != pr.roomId } ?: true } else true diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt index 76c3816ce6..d9a10d8745 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt @@ -34,6 +34,7 @@ import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityRoomDetailBinding import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.navigation.Navigator import im.vector.app.features.room.RequireActiveMembershipAction @@ -102,16 +103,16 @@ class RoomDetailActivity : super.onCreate(savedInstanceState) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) waitingView = views.waitingView.waitingView - val roomDetailArgs: RoomDetailArgs? = if (intent?.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) { - RoomDetailArgs(roomId = intent?.extras?.getString(EXTRA_ROOM_ID)!!) + val timelineArgs: TimelineArgs? = if (intent?.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) { + TimelineArgs(roomId = intent?.extras?.getString(EXTRA_ROOM_ID)!!) } else { intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) } - if (roomDetailArgs == null) return - currentRoomId = roomDetailArgs.roomId + if (timelineArgs == null) return + currentRoomId = timelineArgs.roomId if (isFirstCreation()) { - replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs) + replaceFragment(R.id.roomDetailContainer, TimelineFragment::class.java, timelineArgs) replaceFragment(R.id.roomDetailDrawerContainer, BreadcrumbsFragment::class.java) } @@ -147,7 +148,7 @@ class RoomDetailActivity : if (currentRoomId != switchToRoom.roomId) { currentRoomId = switchToRoom.roomId requireActiveMembershipViewModel.handle(RequireActiveMembershipAction.ChangeRoom(switchToRoom.roomId)) - replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, RoomDetailArgs(switchToRoom.roomId)) + replaceFragment(R.id.roomDetailContainer, TimelineFragment::class.java, TimelineArgs(switchToRoom.roomId)) } } @@ -191,9 +192,9 @@ class RoomDetailActivity : const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID" const val ACTION_ROOM_DETAILS_FROM_SHORTCUT = "ROOM_DETAILS_FROM_SHORTCUT" - fun newIntent(context: Context, roomDetailArgs: RoomDetailArgs): Intent { + fun newIntent(context: Context, timelineArgs: TimelineArgs): Intent { return Intent(context, RoomDetailActivity::class.java).apply { - putExtra(EXTRA_ROOM_DETAIL_ARGS, roomDetailArgs) + putExtra(EXTRA_ROOM_DETAIL_ARGS, timelineArgs) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index ca7ed4b6ec..5854d35fb6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -153,7 +153,7 @@ class RoomDetailViewModel @AssistedInject constructor( @JvmStatic override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? { - val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment() + val fragment: TimelineFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.roomDetailViewModelFactory.create(state) } @@ -668,20 +668,30 @@ class RoomDetailViewModel @AssistedInject constructor( private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled() fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state -> + if (state.asyncRoomSummary()?.membership != Membership.JOIN) { return@withState false } - when (itemId) { - R.id.timeline_setting -> true - R.id.invite -> state.canInvite - R.id.open_matrix_apps -> true - R.id.voice_call -> state.isWebRTCCallOptionAvailable() - R.id.video_call -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined - // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ - R.id.join_conference -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined - R.id.search -> true - R.id.dev_tools -> vectorPreferences.developerMode() - else -> false + + if (initialState.isThreadTimeline()) { + when (itemId) { + R.id.menu_thread_timeline_more -> true + else -> false + } + } else { + when (itemId) { + R.id.timeline_setting -> true + R.id.invite -> state.canInvite + R.id.open_matrix_apps -> true + R.id.voice_call -> state.isWebRTCCallOptionAvailable() + R.id.video_call -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined + // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ + R.id.join_conference -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined + R.id.search -> true + R.id.threads -> true + R.id.dev_tools -> vectorPreferences.developerMode() + else -> false + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 1848f1b28e..fa772ca073 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.home.room.detail.arguments.TimelineArgs 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.initsync.SyncStatusService @@ -69,12 +70,12 @@ data class RoomDetailViewState( val rootThreadEventId: String? = null ) : MavericksState { - constructor(args: RoomDetailArgs) : this( + constructor(args: TimelineArgs) : this( roomId = args.roomId, eventId = args.eventId, // Also highlight the target event, if any highlightedEventId = args.eventId, - rootThreadEventId = args.roomThreadDetailArgs?.eventId + rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId ) fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2 @@ -84,4 +85,7 @@ data class RoomDetailViewState( fun hasActiveJitsiWidget() = activeRoomWidgets()?.any { it.type == WidgetType.Jitsi && it.isActive }.orFalse() fun isDm() = asyncRoomSummary()?.isDirect == true + + fun isThreadTimeline() = rootThreadEventId != null + } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt similarity index 93% rename from vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt rename to vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 6dfe29cec6..9afd5a2fc9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -25,7 +25,6 @@ import android.graphics.Typeface import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Parcelable import android.text.Spannable import android.text.format.DateUtils import android.view.HapticFeedbackConstants @@ -49,7 +48,6 @@ import androidx.core.text.toSpannable import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach -import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.fragment.app.setFragmentResultListener @@ -134,6 +132,7 @@ import im.vector.app.features.command.Command import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.composer.SendMode import im.vector.app.features.home.room.detail.composer.TextComposerAction import im.vector.app.features.home.room.detail.composer.TextComposerView @@ -162,8 +161,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.detail.arguments.RoomThreadDetailArgs -import im.vector.app.features.home.room.threads.detail.RoomThreadDetailActivity +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan import im.vector.app.features.html.PillsPostProcessor @@ -188,7 +186,6 @@ import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize import nl.dionsegijn.konfetti.models.Shape import nl.dionsegijn.konfetti.models.Size import org.billcarsonfr.jsonviewer.JSonViewerDialog @@ -224,16 +221,7 @@ import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject -@Parcelize -data class RoomDetailArgs( - val roomId: String, - val eventId: String? = null, - val sharedData: SharedData? = null, - val openShareSpaceForId: String? = null, - val roomThreadDetailArgs: RoomThreadDetailArgs? = null -) : Parcelable - -class RoomDetailFragment @Inject constructor( +class TimelineFragment @Inject constructor( private val session: Session, private val avatarRenderer: AvatarRenderer, private val timelineEventController: TimelineEventController, @@ -282,16 +270,16 @@ class RoomDetailFragment @Inject constructor( private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) - private val roomDetailArgs: RoomDetailArgs by args() + private val timelineArgs: TimelineArgs by args() private val glideRequests by lazy { GlideApp.with(this) } private val pillsPostProcessor by lazy { - pillsPostProcessorFactory.create(roomDetailArgs.roomId) + pillsPostProcessorFactory.create(timelineArgs.roomId) } private val autoCompleter: AutoCompleter by lazy { - autoCompleterFactory.create(roomDetailArgs.roomId, isThreadTimeLine()) + autoCompleterFactory.create(timelineArgs.roomId, isThreadTimeLine()) } private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() @@ -308,6 +296,8 @@ class RoomDetailFragment @Inject constructor( override fun getMenuRes() = R.menu.menu_timeline private lateinit var sharedActionViewModel: MessageSharedActionViewModel + private lateinit var sharedActivityActionViewModel: RoomDetailSharedActionViewModel + private lateinit var knownCallsViewModel: SharedKnownCallsViewModel private lateinit var layoutManager: LinearLayoutManager @@ -341,10 +331,11 @@ class RoomDetailFragment @Inject constructor( lifecycle.addObserver(ConferenceEventObserver(vectorBaseActivity, this::onBroadcastJitsiEvent)) super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) + sharedActivityActionViewModel = activityViewModelProvider.get(RoomDetailSharedActionViewModel::class.java) knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) attachmentsHelper = AttachmentsHelper(requireContext(), this).register() callActionsHandler = StartCallActionsHandler( - roomId = roomDetailArgs.roomId, + roomId = timelineArgs.roomId, fragment = this, vectorPreferences = vectorPreferences, roomDetailViewModel = roomDetailViewModel, @@ -355,11 +346,7 @@ class RoomDetailFragment @Inject constructor( ) keyboardStateUtils = KeyboardStateUtils(requireActivity()) lazyLoadedViews.bind(views) - if (isThreadTimeLine()) { - views.roomToolbar.isGone = true - } else { - setupToolbar(views.roomToolbar) - } + setupToolbar(views.roomToolbar) setupRecyclerView() setupComposer() setupNotificationView() @@ -370,8 +357,8 @@ class RoomDetailFragment @Inject constructor( setupRemoveJitsiWidgetView() setupVoiceMessageView() - views.roomToolbarContentView.debouncedClicks { - navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) + views.includeRoomToolbar.roomToolbarContentView.debouncedClicks { + navigator.openRoomProfile(requireActivity(), timelineArgs.roomId) } sharedActionViewModel @@ -456,7 +443,7 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) - RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId) + RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), timelineArgs.roomId) RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show() RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings() is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> @@ -532,7 +519,7 @@ class RoomDetailFragment @Inject constructor( private fun handleShowRoomUpgradeDialog(roomDetailViewEvents: TextComposerViewEvents.ShowRoomUpgradeDialog) { val tag = MigrateRoomBottomSheet::javaClass.name - MigrateRoomBottomSheet.newInstance(roomDetailArgs.roomId, roomDetailViewEvents.newVersion) + MigrateRoomBottomSheet.newInstance(timelineArgs.roomId, roomDetailViewEvents.newVersion) .show(parentFragmentManager, tag) } @@ -578,7 +565,7 @@ class RoomDetailFragment @Inject constructor( private fun handleOpenRoomSettings() { navigator.openRoomProfile( requireContext(), - roomDetailArgs.roomId, + timelineArgs.roomId, RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS ) } @@ -600,7 +587,7 @@ class RoomDetailFragment @Inject constructor( WidgetArgs( baseUrl = it.domain, kind = WidgetKind.ROOM, - roomId = roomDetailArgs.roomId, + roomId = timelineArgs.roomId, widgetId = it.widget.widgetId ) ).apply { @@ -626,7 +613,7 @@ class RoomDetailFragment @Inject constructor( navigator.openIntegrationManager( context = requireContext(), activityResultLauncher = integrationManagerActivityResultLauncher, - roomId = roomDetailArgs.roomId, + roomId = timelineArgs.roomId, integId = null, screen = screen ) @@ -717,11 +704,11 @@ class RoomDetailFragment @Inject constructor( } private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) { - navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo)) + navigator.openRoomWidget(requireContext(), timelineArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo)) } private fun openStickerPicker(event: RoomDetailViewEvents.OpenStickerPicker) { - navigator.openStickerPicker(requireContext(), stickerActivityResultLauncher, roomDetailArgs.roomId, event.widget) + navigator.openStickerPicker(requireContext(), stickerActivityResultLauncher, timelineArgs.roomId, event.widget) } private fun startOpenFileIntent(action: RoomDetailViewEvents.OpenFile) { @@ -795,7 +782,7 @@ class RoomDetailFragment @Inject constructor( } private fun handleShareData() { - when (val sharedData = roomDetailArgs.sharedData) { + when (val sharedData = timelineArgs.sharedData) { is SharedData.Text -> { textComposerViewModel.handle(TextComposerAction.EnterRegularMode(sharedData.text, fromSharing = true)) } @@ -808,7 +795,7 @@ class RoomDetailFragment @Inject constructor( } private fun handleSpaceShare() { - roomDetailArgs.openShareSpaceForId?.let { spaceId -> + timelineArgs.openShareSpaceForId?.let { spaceId -> ShareSpaceBottomSheet.show(childFragmentManager, spaceId, true) view?.post { handleChatEffect(ChatEffect.CONFETTI) @@ -909,7 +896,6 @@ class RoomDetailFragment @Inject constructor( override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) - if (isThreadTimeLine()) return // We use a custom layout for this menu item, so we need to set a ClickListener menu.findItem(R.id.open_matrix_apps)?.let { menuItem -> menuItem.actionView.setOnClickListener { @@ -923,15 +909,11 @@ class RoomDetailFragment @Inject constructor( } override fun onPrepareOptionsMenu(menu: Menu) { - if (isThreadTimeLine()) { - menu.forEach { - it.isVisible = false - } - return - } + menu.forEach { it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId) } + withState(roomDetailViewModel) { state -> // Set the visual state of the call buttons (voice/video) to enabled/disabled according to user permissions val hasCallInRoom = callManager.getCallsByRoomId(state.roomId).isNotEmpty() || state.jitsiState.hasJoined @@ -968,41 +950,72 @@ class RoomDetailFragment @Inject constructor( override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { - R.id.invite -> { - navigator.openInviteUsersToRoom(requireActivity(), roomDetailArgs.roomId) + R.id.invite -> { + navigator.openInviteUsersToRoom(requireActivity(), timelineArgs.roomId) true } - R.id.timeline_setting -> { - navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) + R.id.timeline_setting -> { + navigator.openRoomProfile(requireActivity(), timelineArgs.roomId) true } - R.id.open_matrix_apps -> { + R.id.open_matrix_apps -> { roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations) true } - R.id.voice_call -> { + R.id.voice_call -> { callActionsHandler.onVoiceCallClicked() true } - R.id.video_call -> { + R.id.video_call -> { callActionsHandler.onVideoCallClicked() true } - R.id.search -> { + R.id.threads -> { + requireActivity().toast("View All Threads") + true + } + R.id.search -> { handleSearchAction() true } - R.id.dev_tools -> { - navigator.openDevTools(requireContext(), roomDetailArgs.roomId) + R.id.dev_tools -> { + navigator.openDevTools(requireContext(), timelineArgs.roomId) true } - else -> super.onOptionsItemSelected(item) + R.id.menu_thread_timeline_copy_link -> { + requireActivity().toast("menu_thread_timeline_copy_link") + true + } + R.id.menu_thread_timeline_view_in_room -> { + handleViewInRoomAction() + true + } + R.id.menu_thread_timeline_share -> { + requireActivity().toast("menu_thread_timeline_share") + true + } + else -> super.onOptionsItemSelected(item) + } + } + + /** + * View and highlight the original root thread message in the main timeline + */ + private fun handleViewInRoomAction(){ + getRootThreadEventId()?.let { + val newRoom = timelineArgs.copy(threadTimelineArgs = null,eventId = it) + context?.let{ con -> + val int = RoomDetailActivity.newIntent(con, newRoom) + int.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + con.startActivity(int) + + } } } private fun handleSearchAction() { - if (session.getRoom(roomDetailArgs.roomId)?.isEncrypted() == false) { - navigator.openSearch(requireContext(), roomDetailArgs.roomId) + if (session.getRoom(timelineArgs.roomId)?.isEncrypted() == false) { + navigator.openSearch(requireContext(), timelineArgs.roomId) } else { showDialogWithMessage(getString(R.string.search_is_not_supported_in_e2e_room)) } @@ -1080,7 +1093,7 @@ class RoomDetailFragment @Inject constructor( override fun onResume() { super.onResume() - notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId) + notificationDrawerManager.setCurrentRoom(timelineArgs.roomId) roomDetailPendingActionStore.data?.let { handlePendingAction(it) } roomDetailPendingActionStore.data = null @@ -1322,7 +1335,7 @@ class RoomDetailFragment @Inject constructor( views.composerLayout.callback = object : TextComposerView.Callback { override fun onAddAttachment() { if (!::attachmentTypeSelector.isInitialized) { - attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@RoomDetailFragment) + attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@TimelineFragment) } attachmentTypeSelector.show(views.composerLayout.views.attachmentButton, keyboardStateUtils.isKeyboardShowing) } @@ -1420,7 +1433,7 @@ class RoomDetailFragment @Inject constructor( } else if (summary?.membership == Membership.INVITE && inviter != null) { views.hideComposerViews() lazyLoadedViews.inviteView(true)?.apply { - callback = this@RoomDetailFragment + callback = this@TimelineFragment isVisible = true render(inviter, VectorInviteView.Mode.LARGE, mainState.changeMembershipState) setOnClickListener { } @@ -1437,23 +1450,35 @@ class RoomDetailFragment @Inject constructor( } private fun renderToolbar(roomSummary: RoomSummary?, typingMessage: String?) { - if (roomSummary == null) { - views.roomToolbarContentView.isClickable = false + if (!isThreadTimeLine()) { + views.includeRoomToolbar.roomToolbarContentView.isVisible = true + views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false + if (roomSummary == null) { + views.includeRoomToolbar.roomToolbarContentView.isClickable = false + } else { + views.includeRoomToolbar.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN + views.includeRoomToolbar.roomToolbarTitleView.text = roomSummary.displayName + avatarRenderer.render(roomSummary.toMatrixItem(), views.includeRoomToolbar.roomToolbarAvatarImageView) + renderSubTitle(typingMessage, roomSummary.topic) + views.includeRoomToolbar.roomToolbarDecorationImageView.render(roomSummary.roomEncryptionTrustLevel) + views.includeRoomToolbar.roomToolbarPresenceImageView.render(roomSummary.isDirect, roomSummary.directUserPresence) + views.includeRoomToolbar.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect + } } else { - views.roomToolbarContentView.isClickable = roomSummary.membership == Membership.JOIN - views.roomToolbarTitleView.text = roomSummary.displayName - avatarRenderer.render(roomSummary.toMatrixItem(), views.roomToolbarAvatarImageView) - renderSubTitle(typingMessage, roomSummary.topic) - views.roomToolbarDecorationImageView.render(roomSummary.roomEncryptionTrustLevel) - views.roomToolbarPresenceImageView.render(roomSummary.isDirect, roomSummary.directUserPresence) - views.roomToolbarPublicImageView.isVisible = roomSummary.isPublic && !roomSummary.isDirect + views.includeRoomToolbar.roomToolbarContentView.isVisible = false + views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = true + timelineArgs.threadTimelineArgs?.let { + val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl) + avatarRenderer.render(matrixItem, views.includeThreadToolbar.roomToolbarThreadImageView) + views.includeThreadToolbar.roomToolbarThreadSubtitleTextView.text = it.displayName + } } } private fun renderSubTitle(typingMessage: String?, topic: String) { // TODO Temporary place to put typing data val subtitle = typingMessage?.takeIf { it.isNotBlank() } ?: topic - views.roomToolbarSubtitleView.apply { + views.includeRoomToolbar.roomToolbarSubtitleView.apply { setTextOrHide(subtitle) if (typingMessage.isNullOrBlank()) { setTextColor(colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) @@ -1592,14 +1617,14 @@ class RoomDetailFragment @Inject constructor( is RoomDetailAction.RequestVerification -> { Timber.v("## SAS RequestVerification action") VerificationBottomSheet.withArgs( - roomDetailArgs.roomId, + timelineArgs.roomId, data.userId ).show(parentFragmentManager, "REQ") } is RoomDetailAction.AcceptVerificationRequest -> { Timber.v("## SAS AcceptVerificationRequest action") VerificationBottomSheet.withArgs( - roomDetailArgs.roomId, + timelineArgs.roomId, data.otherUserId, data.transactionId ).show(parentFragmentManager, "REQ") @@ -1609,7 +1634,7 @@ class RoomDetailFragment @Inject constructor( VerificationBottomSheet().apply { arguments = Bundle().apply { putParcelable(Mavericks.KEY_ARG, VerificationBottomSheet.VerificationArgs( - otherUserId, data.transactionId, roomId = roomDetailArgs.roomId)) + otherUserId, data.transactionId, roomId = timelineArgs.roomId)) } }.show(parentFragmentManager, "REQ") } @@ -1624,7 +1649,7 @@ class RoomDetailFragment @Inject constructor( .launch(requireActivity(), url, object : NavigationInterceptor { override fun navToRoom(roomId: String?, eventId: String?, deepLink: Uri?): Boolean { // Same room? - if (roomId == roomDetailArgs.roomId) { + if (roomId == timelineArgs.roomId) { // Navigation to same room if (eventId == null) { showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) @@ -1691,7 +1716,7 @@ class RoomDetailFragment @Inject constructor( override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) { navigator.openMediaViewer( activity = requireActivity(), - roomId = roomDetailArgs.roomId, + roomId = timelineArgs.roomId, mediaData = mediaData, view = view ) { pairs -> @@ -1703,7 +1728,7 @@ class RoomDetailFragment @Inject constructor( override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { navigator.openMediaViewer( activity = requireActivity(), - roomId = roomDetailArgs.roomId, + roomId = timelineArgs.roomId, mediaData = mediaData, view = view ) { pairs -> @@ -1738,7 +1763,7 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction)) } - override fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View) { + override fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View, isRootThreadEvent: Boolean) { when (messageContent) { is MessageVerificationRequestContent -> { roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null)) @@ -1751,11 +1776,14 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) } } + if (isRootThreadEvent) { + navigateToThreadTimeline(informationData.eventId) + } } override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean { view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - val roomId = roomDetailArgs.roomId + val roomId = timelineArgs.roomId this.view?.hideKeyboard() MessageActionsBottomSheet @@ -1786,7 +1814,7 @@ class RoomDetailFragment @Inject constructor( } private fun openRoomMemberProfile(userId: String) { - navigator.openRoomMemberProfile(userId = userId, roomId = roomDetailArgs.roomId, context = requireActivity()) + navigator.openRoomMemberProfile(userId = userId, roomId = timelineArgs.roomId, context = requireActivity()) } override fun onMemberNameClicked(informationData: MessageInformationData) { @@ -1804,12 +1832,12 @@ class RoomDetailFragment @Inject constructor( } override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) { - ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + ViewReactionsBottomSheet.newInstance(timelineArgs.roomId, informationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } override fun onEditedDecorationClicked(informationData: MessageInformationData) { - ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData) + ViewEditHistoryBottomSheet.newInstance(timelineArgs.roomId, informationData) .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") } @@ -1921,7 +1949,7 @@ class RoomDetailFragment @Inject constructor( emojiActivityResultLauncher.launch(EmojiReactionPickerActivity.intent(requireContext(), action.eventId)) } is EventSharedAction.ViewReactions -> { - ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) + ViewReactionsBottomSheet.newInstance(timelineArgs.roomId, action.messageInformationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } is EventSharedAction.Copy -> { @@ -1978,20 +2006,13 @@ class RoomDetailFragment @Inject constructor( } is EventSharedAction.ReplyInThread -> { if (!views.voiceMessageRecorderView.isActive()) { - context?.let { - val roomThreadDetailArgs = RoomThreadDetailArgs( - roomId = roomDetailArgs.roomId, - displayName = roomDetailViewModel.getRoomSummary()?.displayName, - avatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl, - eventId = action.eventId) - startActivity(RoomThreadDetailActivity.newIntent(it, roomThreadDetailArgs)) - } + navigateToThreadTimeline(action.eventId) } else { requireActivity().toast(R.string.error_voice_message_cannot_reply_or_edit) } } is EventSharedAction.CopyPermalink -> { - val permalink = session.permalinkService().createPermalink(roomDetailArgs.roomId, action.eventId) + val permalink = session.permalinkService().createPermalink(timelineArgs.roomId, action.eventId) copyToClipboard(requireContext(), permalink, false) showSnackWithMessage(getString(R.string.copied_to_clipboard)) } @@ -2114,15 +2135,31 @@ class RoomDetailFragment @Inject constructor( .show() } + /** + * Navigate to Threads timeline for the specified threadRootEventId + * using the RoomThreadDetailActivity + */ + + private fun navigateToThreadTimeline(rootThreadEventId: String) { + context?.let { + val roomThreadDetailArgs = ThreadTimelineArgs( + roomId = timelineArgs.roomId, + displayName = roomDetailViewModel.getRoomSummary()?.displayName, + avatarUrl = roomDetailViewModel.getRoomSummary()?.avatarUrl, + rootThreadEventId = rootThreadEventId) + navigator.openThread(it, roomThreadDetailArgs) + } + } + // VectorInviteView.Callback override fun onAcceptInvite() { - notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) + notificationDrawerManager.clearMemberShipNotificationForRoom(timelineArgs.roomId) roomDetailViewModel.handle(RoomDetailAction.AcceptInvite) } override fun onRejectInvite() { - notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) + notificationDrawerManager.clearMemberShipNotificationForRoom(timelineArgs.roomId) roomDetailViewModel.handle(RoomDetailAction.RejectInvite) } @@ -2221,6 +2258,13 @@ class RoomDetailFragment @Inject constructor( } } - private fun isThreadTimeLine(): Boolean = roomDetailArgs.roomThreadDetailArgs != null - fun getRootThreadEventId(): String? = roomDetailArgs.roomThreadDetailArgs?.eventId + /** + * Returns true if the current room is a Thread room, false otherwise + */ + private fun isThreadTimeLine(): Boolean = timelineArgs.threadTimelineArgs?.rootThreadEventId != null + + /** + * Returns the root thread event if we are in a thread room, otherwise returns null + */ + fun getRootThreadEventId(): String? = timelineArgs.threadTimelineArgs?.rootThreadEventId } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt new file mode 100644 index 0000000000..26455e04c7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021 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.detail.arguments + +import android.os.Parcelable +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs +import im.vector.app.features.share.SharedData +import kotlinx.parcelize.Parcelize + +@Parcelize +data class TimelineArgs( + val roomId: String, + val eventId: String? = null, + val sharedData: SharedData? = null, + val openShareSpaceForId: String? = null, + val threadTimelineArgs: ThreadTimelineArgs? = null +) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt index bcc26247a2..af870c3313 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewModel.kt @@ -29,7 +29,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.command.CommandParser import im.vector.app.features.command.ParsedCommand import im.vector.app.features.home.room.detail.ChatEffect -import im.vector.app.features.home.room.detail.RoomDetailFragment +import im.vector.app.features.home.room.detail.TimelineFragment import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.toMessageType import im.vector.app.features.powerlevel.PowerLevelsFlowFactory @@ -42,7 +42,6 @@ import org.commonmark.renderer.html.HtmlRenderer import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent @@ -772,7 +771,7 @@ class TextComposerViewModel @AssistedInject constructor( @JvmStatic override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel { - val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment() + val fragment: TimelineFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.textComposerViewModelFactory.create(state) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt index f4dd5adebe..0e8d9e1e86 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerViewState.kt @@ -17,7 +17,7 @@ package im.vector.app.features.home.room.detail.composer import com.airbnb.mvrx.MavericksState -import im.vector.app.features.home.room.detail.RoomDetailArgs +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent /** @@ -53,9 +53,9 @@ data class TextComposerViewState( val isComposerVisible: Boolean get() = canSendMessage && !isVoiceRecording - constructor(args: RoomDetailArgs) : this( + constructor(args: TimelineArgs) : this( roomId = args.roomId, - rootThreadEventId = args.roomThreadDetailArgs?.eventId) + rootThreadEventId = args.threadTimelineArgs?.rootThreadEventId) fun isInThreadTimeline(): Boolean = rootThreadEventId != null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index d08259d739..11c90b3482 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -140,7 +140,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } interface BaseCallback { - fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View) + fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View, isRootThreadEvent: Boolean) fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt index 11061cbc9a..a30a0b851e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -43,7 +43,7 @@ class MessageItemAttributesFactory @Inject constructor( callback?.onEventLongClicked(informationData, messageContent, view) ?: false }, itemClickListener = { view -> - callback?.onEventCellClicked(informationData, messageContent, view) + callback?.onEventCellClicked(informationData, messageContent, view, threadDetails?.isRootThread ?: false) }, memberClickListener = { callback?.onMemberNameClicked(informationData) diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/RoomThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/RoomThreadsActivity.kt deleted file mode 100644 index 0ad1d02ffb..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/RoomThreadsActivity.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2021 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.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.appcompat.widget.SearchView -import im.vector.app.R -import im.vector.app.core.di.ScreenComponent -import im.vector.app.core.extensions.replaceFragment -import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.databinding.ActivityFilteredRoomsBinding -import im.vector.app.databinding.ActivityRoomThreadsBinding -import im.vector.app.features.home.RoomListDisplayMode -import im.vector.app.features.home.room.list.RoomListFragment -import im.vector.app.features.home.room.list.RoomListParams - -class RoomThreadsActivity : VectorBaseActivity() { - -// private val roomListFragment: RoomListFragment? -// get() { -// return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomListFragment -// } - - override fun getBinding() = ActivityRoomThreadsBinding.inflate(layoutInflater) - - override fun getCoordinatorLayout() = views.coordinatorLayout - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun getMenuRes() = R.menu.menu_room_threads - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - configureToolbar(views.roomThreadsToolbar) -// if (isFirstCreation()) { -// val params = RoomListParams(RoomListDisplayMode.FILTERED) -// replaceFragment(R.id.filteredRoomsFragmentContainer, RoomListFragment::class.java, params, FRAGMENT_TAG) -// } - } - - companion object { - private const val FRAGMENT_TAG = "RoomListFragment" - - fun newIntent(context: Context): Intent { - return Intent(context, RoomThreadsActivity::class.java) - } - } -} 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 new file mode 100644 index 0000000000..73da1354af --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2021 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.content.Context +import android.content.Intent +import android.os.Bundle +import com.google.android.material.appbar.MaterialToolbar +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.replaceFragment +import im.vector.app.core.platform.ToolbarConfigurable +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.databinding.ActivityThreadsBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.arguments.TimelineArgs +import im.vector.app.features.home.room.detail.TimelineFragment +import im.vector.app.features.home.room.threads.arguments.ThreadListArgs +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs +import im.vector.app.features.home.room.threads.detail.ThreadListFragment +import javax.inject.Inject + +class ThreadsActivity : VectorBaseActivity(), ToolbarConfigurable { + + @Inject + lateinit var avatarRenderer: AvatarRenderer + +// private val roomThreadDetailFragment: RoomThreadDetailFragment? +// get() { +// return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomThreadDetailFragment +// } + + override fun getBinding() = ActivityThreadsBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initFragment() + } + + private fun initFragment() { + if (isFirstCreation()) { + when (val fragment = fragmentToNavigate()) { + is DisplayFragment.ThreadList -> { + initThreadListFragment(fragment.threadListArgs) + } + is DisplayFragment.ThreadTimeLine -> { + initThreadTimelineFragment(fragment.threadTimelineArgs) + } + is DisplayFragment.ErrorFragment -> { + finish() + } + } + } + } + + private fun initThreadListFragment(threadListArgs: ThreadListArgs) { + replaceFragment( + R.id.threadsActivityFragmentContainer, + ThreadListFragment::class.java, + threadListArgs) + } + + private fun initThreadTimelineFragment(threadTimelineArgs: ThreadTimelineArgs) = + replaceFragment( + R.id.threadsActivityFragmentContainer, + TimelineFragment::class.java, + TimelineArgs( + roomId = threadTimelineArgs.roomId, + threadTimelineArgs = threadTimelineArgs + )) + + override fun configure(toolbar: MaterialToolbar) { + configureToolbar(toolbar) + } + + /** + * Determine in witch fragment we should navigate + */ + private fun fragmentToNavigate(): DisplayFragment { + getThreadTimelineArgs()?.let { + return DisplayFragment.ThreadTimeLine(it) + } + getThreadListArgs()?.let { + return DisplayFragment.ThreadList(it) + } + return DisplayFragment.ErrorFragment + } + + private fun getThreadTimelineArgs(): ThreadTimelineArgs? = intent?.extras?.getParcelable(THREAD_TIMELINE_ARGS) + private fun getThreadListArgs(): ThreadListArgs? = intent?.extras?.getParcelable(THREAD_LIST_ARGS) + + companion object { + // private val FRAGMENT_TAG = RoomThreadDetailFragment::class.simpleName + const val THREAD_TIMELINE_ARGS = "THREAD_TIMELINE_ARGS" + const val THREAD_LIST_ARGS = "THREAD_LIST_ARGS" + + fun newIntent(context: Context, threadTimelineArgs: ThreadTimelineArgs?, threadListArgs: ThreadListArgs?): Intent { + return Intent(context, ThreadsActivity::class.java).apply { + putExtra(THREAD_TIMELINE_ARGS, threadTimelineArgs) + putExtra(THREAD_LIST_ARGS, threadListArgs) + + } + } + } + + sealed class DisplayFragment { + data class ThreadList(val threadListArgs: ThreadListArgs) : DisplayFragment() + data class ThreadTimeLine(val threadTimelineArgs: ThreadTimelineArgs) : DisplayFragment() + object ErrorFragment : DisplayFragment() + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt new file mode 100644 index 0000000000..23b72e5f32 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadListArgs.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 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.arguments + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ThreadListArgs( + val roomId: String +) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/arguments/RoomThreadDetailArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt similarity index 85% rename from vector/src/main/java/im/vector/app/features/home/room/threads/detail/arguments/RoomThreadDetailArgs.kt rename to vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt index 5fce24cd0d..2ebed2f745 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/arguments/RoomThreadDetailArgs.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/arguments/ThreadTimelineArgs.kt @@ -14,15 +14,15 @@ * limitations under the License. */ -package im.vector.app.features.home.room.threads.detail.arguments +package im.vector.app.features.home.room.threads.arguments import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class RoomThreadDetailArgs( +data class ThreadTimelineArgs( val roomId: String, val displayName: String?, val avatarUrl: String?, - val eventId: String? = null, + val rootThreadEventId: String? = null ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailActivity.kt deleted file mode 100644 index c82fa353e4..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailActivity.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2021 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.detail - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import im.vector.app.R -import im.vector.app.core.di.ScreenComponent -import im.vector.app.core.extensions.replaceFragment -import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.databinding.ActivityRoomThreadDetailBinding -import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.detail.RoomDetailArgs -import im.vector.app.features.home.room.detail.RoomDetailFragment -import im.vector.app.features.home.room.threads.detail.arguments.RoomThreadDetailArgs -import org.matrix.android.sdk.api.util.MatrixItem -import javax.inject.Inject - -class RoomThreadDetailActivity : VectorBaseActivity() { - - @Inject - lateinit var avatarRenderer: AvatarRenderer - -// private val roomThreadDetailFragment: RoomThreadDetailFragment? -// get() { -// return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomThreadDetailFragment -// } - - override fun getBinding() = ActivityRoomThreadDetailBinding.inflate(layoutInflater) - - override fun getCoordinatorLayout() = views.coordinatorLayout - - override fun injectWith(injector: ScreenComponent) { - injector.inject(this) - } - - override fun getMenuRes() = R.menu.menu_room_threads - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - initToolbar() - initFragment() - } - - private fun initToolbar() { - configureToolbar(views.roomThreadDetailToolbar) - getRoomThreadDetailArgs()?.let { - val matrixItem = MatrixItem.RoomItem(it.roomId, it.displayName, it.avatarUrl) - avatarRenderer.render(matrixItem, views.roomThreadDetailToolbarImageView) - } - } - - private fun initFragment() { - if (isFirstCreation()) { - getRoomThreadDetailArgs()?.let { - replaceFragment( - R.id.roomThreadDetailFragmentContainer, - RoomDetailFragment::class.java, - RoomDetailArgs( - roomId = it.roomId, - roomThreadDetailArgs = it - )) - } -// replaceFragment(R.id.roomThreadDetailFragmentContainer, RoomThreadDetailFragment::class.java, getRoomThreadDetailArgs(), FRAGMENT_TAG) - } - } - - private fun getRoomThreadDetailArgs(): RoomThreadDetailArgs? = intent?.extras?.getParcelable(ROOM_THREAD_DETAIL_ARGS) - - companion object { - private val FRAGMENT_TAG = RoomThreadDetailFragment::class.simpleName - const val ROOM_THREAD_DETAIL_ARGS = "ROOM_THREAD_DETAIL_ARGS" - - fun newIntent(context: Context, roomThreadDetailArgs: RoomThreadDetailArgs): Intent { - return Intent(context, RoomThreadDetailActivity::class.java).apply { - putExtra(ROOM_THREAD_DETAIL_ARGS, roomThreadDetailArgs) - } - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/ThreadListFragment.kt similarity index 74% rename from vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt rename to vector/src/main/java/im/vector/app/features/home/room/threads/detail/ThreadListFragment.kt index fee21128cd..4e870bd53b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/detail/RoomThreadDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/detail/ThreadListFragment.kt @@ -16,34 +16,31 @@ package im.vector.app.features.home.room.threads.detail -import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.isVisible import com.airbnb.mvrx.args import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.databinding.FragmentRoomThreadDetailBinding -import im.vector.app.features.home.room.threads.detail.arguments.RoomThreadDetailArgs +import im.vector.app.databinding.FragmentThreadListBinding +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import org.matrix.android.sdk.api.session.Session import javax.inject.Inject -class RoomThreadDetailFragment @Inject constructor( +class ThreadListFragment @Inject constructor( private val session: Session -) : VectorBaseFragment() { +) : VectorBaseFragment() { - private val roomThreadDetailArgs: RoomThreadDetailArgs by args() + private val threadTimelineArgs: ThreadTimelineArgs by args() - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomThreadDetailBinding { - return FragmentRoomThreadDetailBinding.inflate(inflater, container, false) + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentThreadListBinding { + return FragmentThreadListBinding.inflate(inflater, container, false) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } - @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initTextComposer() @@ -59,7 +56,7 @@ class RoomThreadDetailFragment @Inject constructor( } private fun initTextComposer(){ - views.roomThreadDetailTextComposerView.views.sendButton.isVisible = true +// views.roomThreadDetailTextComposerView.views.sendButton.isVisible = true } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index debdf3739c..fdf1a24261 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -49,10 +49,13 @@ import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.debug.DebugMenuActivity import im.vector.app.features.devtools.RoomDevToolActivity import im.vector.app.features.home.room.detail.RoomDetailActivity -import im.vector.app.features.home.room.detail.RoomDetailArgs +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.search.SearchActivity import im.vector.app.features.home.room.detail.search.SearchArgs import im.vector.app.features.home.room.filtered.FilteredRoomsActivity +import im.vector.app.features.home.room.threads.ThreadsActivity +import im.vector.app.features.home.room.threads.arguments.ThreadListArgs +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.invite.InviteUsersToRoomActivity import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginConfig @@ -118,7 +121,7 @@ class DefaultNavigator @Inject constructor( fatalError("Trying to open an unknown room $roomId", vectorPreferences.failFast()) return } - val args = RoomDetailArgs(roomId, eventId) + val args = TimelineArgs(roomId, eventId) val intent = RoomDetailActivity.newIntent(context, args) startActivity(context, intent, buildTask) } @@ -141,7 +144,7 @@ class DefaultNavigator @Inject constructor( startActivity(context, SpaceManageActivity.newIntent(context, spaceId, ManageType.AddRooms), false) } is Navigator.PostSwitchSpaceAction.OpenDefaultRoom -> { - val args = RoomDetailArgs( + val args = TimelineArgs( postSwitchSpaceAction.roomId, eventId = null, openShareSpaceForId = spaceId.takeIf { postSwitchSpaceAction.showShareSheet } @@ -239,7 +242,7 @@ class DefaultNavigator @Inject constructor( } override fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData) { - val args = RoomDetailArgs(roomId, null, sharedData) + val args = TimelineArgs(roomId, null, sharedData) val intent = RoomDetailActivity.newIntent(activity, args) activity.startActivity(intent) activity.finish() @@ -507,4 +510,11 @@ class DefaultNavigator @Inject constructor( context.startActivity(intent) } } + + override fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs) { + context.startActivity(ThreadsActivity.newIntent( + context = context, + threadTimelineArgs = threadTimelineArgs, + threadListArgs =null)) + } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 612643c804..eeeb2a1b35 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -24,6 +24,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.core.util.Pair import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.displayname.getBestName +import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.login.LoginConfig import im.vector.app.features.media.AttachmentData import im.vector.app.features.pin.PinMode @@ -140,4 +141,7 @@ interface Navigator { fun openDevTools(context: Context, roomId: String) fun openCallTransfer(context: Context, callId: String) + + fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs) + } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 92feb3d038..7f41049c21 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -56,7 +56,7 @@ import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.room.detail.RoomDetailActivity -import im.vector.app.features.home.room.detail.RoomDetailArgs +import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver import im.vector.app.features.themes.ThemeUtils @@ -497,7 +497,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val contentPendingIntent = TaskStackBuilder.create(context) .addNextIntentWithParentStack(HomeActivity.newIntent(context)) - .addNextIntent(RoomDetailActivity.newIntent(context, RoomDetailArgs(callInformation.nativeRoomId))) + .addNextIntent(RoomDetailActivity.newIntent(context, TimelineArgs(callInformation.nativeRoomId))) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) builder.setContentIntent(contentPendingIntent) @@ -735,7 +735,7 @@ class NotificationUtils @Inject constructor(private val context: Context, } private fun buildOpenRoomIntent(roomId: String): PendingIntent? { - val roomIntentTap = RoomDetailActivity.newIntent(context, RoomDetailArgs(roomId)) + val roomIntentTap = RoomDetailActivity.newIntent(context, TimelineArgs(roomId)) roomIntentTap.action = TAP_TO_VIEW_ACTION // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that roomIntentTap.data = Uri.parse("foobar://openRoom?$roomId") diff --git a/vector/src/main/res/drawable/ic_thread_link_menu_item.xml b/vector/src/main/res/drawable/ic_thread_link_menu_item.xml new file mode 100644 index 0000000000..779c9d832c --- /dev/null +++ b/vector/src/main/res/drawable/ic_thread_link_menu_item.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/drawable/ic_thread_menu_item.xml b/vector/src/main/res/drawable/ic_thread_menu_item.xml new file mode 100644 index 0000000000..2d77251c53 --- /dev/null +++ b/vector/src/main/res/drawable/ic_thread_menu_item.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_thread_share_menu_item.xml b/vector/src/main/res/drawable/ic_thread_share_menu_item.xml new file mode 100644 index 0000000000..cb863c39bf --- /dev/null +++ b/vector/src/main/res/drawable/ic_thread_share_menu_item.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_thread_view_in_room_menu_item.xml b/vector/src/main/res/drawable/ic_thread_view_in_room_menu_item.xml new file mode 100644 index 0000000000..f408f99713 --- /dev/null +++ b/vector/src/main/res/drawable/ic_thread_view_in_room_menu_item.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/vector/src/main/res/layout/activity_room_thread_detail.xml b/vector/src/main/res/layout/activity_room_thread_detail.xml deleted file mode 100644 index 94c52ab959..0000000000 --- a/vector/src/main/res/layout/activity_room_thread_detail.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_room_threads.xml b/vector/src/main/res/layout/activity_room_threads.xml deleted file mode 100644 index b469c7de42..0000000000 --- a/vector/src/main/res/layout/activity_room_threads.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_threads.xml b/vector/src/main/res/layout/activity_threads.xml new file mode 100644 index 0000000000..c34be9687d --- /dev/null +++ b/vector/src/main/res/layout/activity_threads.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 7725cd5e92..5d0e5f3ebd 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -26,101 +26,13 @@ android:layout_height="?actionBarSize" android:transitionName="toolbar"> - + - - - - - - - - - - - - - + diff --git a/vector/src/main/res/layout/fragment_room_thread_detail.xml b/vector/src/main/res/layout/fragment_room_thread_detail.xml deleted file mode 100644 index cadc819d28..0000000000 --- a/vector/src/main/res/layout/fragment_room_thread_detail.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - diff --git a/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml b/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml new file mode 100644 index 0000000000..dcb60a44d7 --- /dev/null +++ b/vector/src/main/res/layout/view_room_detail_thread_toolbar.xml @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/vector/src/main/res/layout/view_room_detail_toolbar.xml b/vector/src/main/res/layout/view_room_detail_toolbar.xml new file mode 100644 index 0000000000..fdc3f6819e --- /dev/null +++ b/vector/src/main/res/layout/view_room_detail_toolbar.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/menu/menu_thread_timeline.xml b/vector/src/main/res/menu/menu_thread_timeline.xml new file mode 100644 index 0000000000..4698559bae --- /dev/null +++ b/vector/src/main/res/menu/menu_thread_timeline.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml index 54967f1706..532b63dd38 100644 --- a/vector/src/main/res/menu/menu_timeline.xml +++ b/vector/src/main/res/menu/menu_timeline.xml @@ -37,11 +37,20 @@ app:showAsAction="always" tools:visible="true" /> - + + + app:showAsAction="always" /> + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index b781692733..a85bf0629e 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -441,6 +441,7 @@ Are you sure you want to sign out? Voice Call Video Call + View Threads Global search Mark all as read Historical @@ -456,6 +457,11 @@ Disable Return + + View in room + Copy link to thread + Share + Confirmation Warning @@ -1023,6 +1029,7 @@ Filter Threads in room + Thread Reason for reporting this content