diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index 3633bb610a..f9ffc9c612 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -77,6 +77,7 @@ import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheet import im.vector.app.features.share.IncomingShareActivity import im.vector.app.features.signout.soft.SoftLogoutActivity +import im.vector.app.features.spaces.ShareSpaceBottomSheet import im.vector.app.features.spaces.SpaceCreationActivity import im.vector.app.features.terms.ReviewTermsActivity import im.vector.app.features.ui.UiStateRepository @@ -175,6 +176,7 @@ interface ScreenComponent { fun inject(bottomSheet: CallControlsBottomSheet) fun inject(bottomSheet: SignOutBottomSheetDialogFragment) fun inject(bottomSheet: MatrixToBottomSheet) + fun inject(bottomSheet: ShareSpaceBottomSheet) /* ========================================================================================== * Others diff --git a/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt index c3f459f49e..f4c95215b8 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/HomeSpaceSummaryItem.kt @@ -22,6 +22,7 @@ import android.util.TypedValue import android.widget.ImageView import android.widget.TextView import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -48,12 +49,14 @@ abstract class HomeSpaceSummaryItem : VectorEpoxyModel(R.id.groupAvatarImageView) val groupNameView by bind(R.id.groupNameView) val rootView by bind(R.id.itemGroupLayout) + val leaveView by bind(R.id.groupTmpLeave) } fun dpToPx(resources: Resources, dp: Int): Int { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 40d0cdd622..db7bb8fa16 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -111,14 +111,10 @@ class HomeActivity : val defaultRoomsId = activityResult.data?.extras?.getString(SpaceCreationActivity.RESULT_DATA_DEFAULT_ROOM_ID) views.drawerLayout.closeDrawer(GravityCompat.START) - // Here we want to change current space to the newly created one, and then immediatly open the default room + // Here we want to change current space to the newly created one, and then immediately open the default room if (spaceId != null) { - navigator.switchToSpace(this, spaceId, defaultRoomsId) + navigator.switchToSpace(this, spaceId, defaultRoomsId, true) } - - // Also we should show the share space bottomsheet - } else { - // viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled) } } 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/RoomDetailFragment.kt index b7e2e189d3..a585d7609a 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/RoomDetailFragment.kt @@ -91,7 +91,9 @@ import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.CurrentCallsView +import im.vector.app.core.ui.views.JumpToReadMarkerView import im.vector.app.core.ui.views.KnownCallsViewHolder import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.FailedMessagesWarningView @@ -163,6 +165,7 @@ import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.share.SharedData +import im.vector.app.features.spaces.ShareSpaceBottomSheet import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgs @@ -210,7 +213,8 @@ import javax.inject.Inject data class RoomDetailArgs( val roomId: String, val eventId: String? = null, - val sharedData: SharedData? = null + val sharedData: SharedData? = null, + val openShareSpaceForId: String? = null ) : Parcelable class RoomDetailFragment @Inject constructor( @@ -293,7 +297,7 @@ class RoomDetailFragment @Inject constructor( private lateinit var attachmentsHelper: AttachmentsHelper private lateinit var keyboardStateUtils: KeyboardStateUtils - private lateinit var callActionsHandler : StartCallActionsHandler + private lateinit var callActionsHandler: StartCallActionsHandler private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView @@ -358,9 +362,9 @@ class RoomDetailFragment @Inject constructor( } when (mode) { is SendMode.REGULAR -> renderRegularMode(mode.text) - is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text) - is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text) - is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text) + is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text) + is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text) + is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text) } } @@ -384,29 +388,30 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager() is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it) is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog() - is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager() - is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it) - RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked() - is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message) - is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo) - RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView() - RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() - is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) - is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) - RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId) - RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show() - RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings() - is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> + is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager() + is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it) + RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked() + is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message) + is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo) + RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView() + RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() + is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) + is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) + RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId) + RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show() + RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings() + is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> navigator.openBigImageViewer(requireActivity(), it.view, item) } - is RoomDetailViewEvents.StartChatEffect -> handleChatEffect(it.type) - RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() - is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) + is RoomDetailViewEvents.StartChatEffect -> handleChatEffect(it.type) + RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() + is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) }.exhaustive } if (savedInstanceState == null) { handleShareData() + handleSpaceShare() } } @@ -419,7 +424,7 @@ class RoomDetailFragment @Inject constructor( startActivity(intent) } - private fun handleChatEffect(chatEffect: ChatEffect) { + private fun handleChatEffect(chatEffect: ChatEffect) { when (chatEffect) { ChatEffect.CONFETTI -> { views.viewKonfetti.isVisible = true @@ -434,7 +439,7 @@ class RoomDetailFragment @Inject constructor( .setPosition(-50f, views.viewKonfetti.width + 50f, -50f, -50f) .streamFor(150, 3000L) } - ChatEffect.SNOW -> { + ChatEffect.SNOW -> { views.viewSnowFall.isVisible = true views.viewSnowFall.restartFalling() } @@ -627,17 +632,26 @@ class RoomDetailFragment @Inject constructor( private fun handleShareData() { when (val sharedData = roomDetailArgs.sharedData) { - is SharedData.Text -> { + is SharedData.Text -> { roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(sharedData.text, fromSharing = true)) } is SharedData.Attachments -> { // open share edition onContentAttachmentsReady(sharedData.attachmentData) } - null -> Timber.v("No share data to process") + null -> Timber.v("No share data to process") }.exhaustive } + private fun handleSpaceShare() { + roomDetailArgs.openShareSpaceForId?.let { spaceId -> + ShareSpaceBottomSheet.show(childFragmentManager, spaceId) + view?.post { + handleChatEffect(ChatEffect.CONFETTI) + } + } + } + override fun onDestroyView() { timelineEventController.callback = null timelineEventController.removeModelBuildListener(modelBuildListener) @@ -760,8 +774,8 @@ class RoomDetailFragment @Inject constructor( withState(roomDetailViewModel) { state -> // Set the visual state of the call buttons (voice/video) to enabled/disabled according to user permissions val callButtonsEnabled = when (state.asyncRoomSummary.invoke()?.joinedMembersCount) { - 1 -> false - 2 -> state.isAllowedToStartWebRTCCall + 1 -> false + 2 -> state.isAllowedToStartWebRTCCall else -> state.isAllowedToManageWidgets } setOf(R.id.voice_call, R.id.video_call).forEach { @@ -791,7 +805,7 @@ class RoomDetailFragment @Inject constructor( override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { - R.id.invite -> { + R.id.invite -> { navigator.openInviteUsersToRoom(requireActivity(), roomDetailArgs.roomId) true } @@ -807,19 +821,19 @@ class RoomDetailFragment @Inject constructor( callActionsHandler.onVoiceCallClicked() true } - R.id.video_call -> { + R.id.video_call -> { callActionsHandler.onVideoCallClicked() true } - R.id.hangup_call -> { + R.id.hangup_call -> { roomDetailViewModel.handle(RoomDetailAction.EndCall) true } - R.id.search -> { + R.id.search -> { handleSearchAction() true } - R.id.dev_tools -> { + R.id.dev_tools -> { navigator.openDevTools(requireContext(), roomDetailArgs.roomId) true } @@ -921,9 +935,9 @@ class RoomDetailFragment @Inject constructor( when (roomDetailPendingAction) { is RoomDetailPendingAction.JumpToReadReceipt -> roomDetailViewModel.handle(RoomDetailAction.JumpToReadReceipt(roomDetailPendingAction.userId)) - is RoomDetailPendingAction.MentionUser -> + is RoomDetailPendingAction.MentionUser -> insertUserDisplayNameInTextEditor(roomDetailPendingAction.userId) - is RoomDetailPendingAction.OpenOrCreateDm -> + is RoomDetailPendingAction.OpenOrCreateDm -> roomDetailViewModel.handle(RoomDetailAction.OpenOrCreateDm(roomDetailPendingAction.userId)) }.exhaustive } @@ -1078,9 +1092,9 @@ class RoomDetailFragment @Inject constructor( withState(roomDetailViewModel) { val showJumpToUnreadBanner = when (it.unreadState) { UnreadState.Unknown, - UnreadState.HasNoUnread -> false + UnreadState.HasNoUnread -> false is UnreadState.ReadMarkerNotLoaded -> true - is UnreadState.HasUnread -> { + is UnreadState.HasUnread -> { if (it.canShowJumpToReadMarker) { val lastVisibleItem = layoutManager.findLastVisibleItemPosition() val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() @@ -1268,7 +1282,7 @@ class RoomDetailFragment @Inject constructor( navigator.openRoom(vectorBaseActivity, async()) vectorBaseActivity.finish() } - is Fail -> { + is Fail -> { vectorBaseActivity.hideWaitingView() vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error)) } @@ -1277,19 +1291,19 @@ class RoomDetailFragment @Inject constructor( private fun renderSendMessageResult(sendMessageResult: RoomDetailViewEvents.SendMessageResult) { when (sendMessageResult) { - is RoomDetailViewEvents.SlashCommandHandled -> { + is RoomDetailViewEvents.SlashCommandHandled -> { sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } } - is RoomDetailViewEvents.SlashCommandError -> { + is RoomDetailViewEvents.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) } - is RoomDetailViewEvents.SlashCommandUnknown -> { + is RoomDetailViewEvents.SlashCommandUnknown -> { displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } - is RoomDetailViewEvents.SlashCommandResultOk -> { + is RoomDetailViewEvents.SlashCommandResultOk -> { updateComposerText("") } - is RoomDetailViewEvents.SlashCommandResultError -> { + is RoomDetailViewEvents.SlashCommandResultError -> { displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable)) } is RoomDetailViewEvents.SlashCommandNotImplemented -> { @@ -1311,7 +1325,7 @@ class RoomDetailFragment @Inject constructor( private fun displayE2eError(withHeldCode: WithHeldCode?) { val msgId = when (withHeldCode) { WithHeldCode.BLACKLISTED -> R.string.crypto_error_withheld_blacklisted - WithHeldCode.UNVERIFIED -> R.string.crypto_error_withheld_unverified + WithHeldCode.UNVERIFIED -> R.string.crypto_error_withheld_unverified WithHeldCode.UNAUTHORISED, WithHeldCode.UNAVAILABLE -> R.string.crypto_error_withheld_generic else -> R.string.notice_crypto_unable_to_decrypt_friendly_desc @@ -1362,7 +1376,7 @@ class RoomDetailFragment @Inject constructor( private fun displayRoomDetailActionSuccess(result: RoomDetailViewEvents.ActionSuccess) { when (val data = result.action) { - is RoomDetailAction.ReportContent -> { + is RoomDetailAction.ReportContent -> { when { data.spam -> { AlertDialog.Builder(requireActivity()) @@ -1399,7 +1413,7 @@ class RoomDetailFragment @Inject constructor( } } } - is RoomDetailAction.RequestVerification -> { + is RoomDetailAction.RequestVerification -> { Timber.v("## SAS RequestVerification action") VerificationBottomSheet.withArgs( roomDetailArgs.roomId, @@ -1414,7 +1428,7 @@ class RoomDetailFragment @Inject constructor( data.transactionId ).show(parentFragmentManager, "REQ") } - is RoomDetailAction.ResumeVerification -> { + is RoomDetailAction.ResumeVerification -> { val otherUserId = data.otherUserId ?: return VerificationBottomSheet().apply { arguments = Bundle().apply { @@ -1557,11 +1571,11 @@ class RoomDetailFragment @Inject constructor( is MessageVerificationRequestContent -> { roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null)) } - is MessageWithAttachmentContent -> { + is MessageWithAttachmentContent -> { val action = RoomDetailAction.DownloadOrOpen(informationData.eventId, informationData.senderId, messageContent) roomDetailViewModel.handle(action) } - is EncryptedEventContent -> { + is EncryptedEventContent -> { roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId)) } } @@ -1724,75 +1738,75 @@ class RoomDetailFragment @Inject constructor( private fun handleActions(action: EventSharedAction) { when (action) { - is EventSharedAction.OpenUserProfile -> { + is EventSharedAction.OpenUserProfile -> { openRoomMemberProfile(action.userId) } - is EventSharedAction.AddReaction -> { + is EventSharedAction.AddReaction -> { emojiActivityResultLauncher.launch(EmojiReactionPickerActivity.intent(requireContext(), action.eventId)) } - is EventSharedAction.ViewReactions -> { + is EventSharedAction.ViewReactions -> { ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } - is EventSharedAction.Copy -> { + is EventSharedAction.Copy -> { // I need info about the current selected message :/ copyToClipboard(requireContext(), action.content, false) showSnackWithMessage(getString(R.string.copied_to_clipboard)) } - is EventSharedAction.Redact -> { + is EventSharedAction.Redact -> { promptConfirmationToRedactEvent(action) } - is EventSharedAction.Share -> { + is EventSharedAction.Share -> { onShareActionClicked(action) } - is EventSharedAction.Save -> { + is EventSharedAction.Save -> { onSaveActionClicked(action) } - is EventSharedAction.ViewEditHistory -> { + is EventSharedAction.ViewEditHistory -> { onEditedDecorationClicked(action.messageInformationData) } - is EventSharedAction.ViewSource -> { + is EventSharedAction.ViewSource -> { JSonViewerDialog.newInstance( action.content, -1, createJSonViewerStyleProvider(colorProvider) ).show(childFragmentManager, "JSON_VIEWER") } - is EventSharedAction.ViewDecryptedSource -> { + is EventSharedAction.ViewDecryptedSource -> { JSonViewerDialog.newInstance( action.content, -1, createJSonViewerStyleProvider(colorProvider) ).show(childFragmentManager, "JSON_VIEWER") } - is EventSharedAction.QuickReact -> { + is EventSharedAction.QuickReact -> { // eventId,ClickedOn,Add roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } - is EventSharedAction.Edit -> { + is EventSharedAction.Edit -> { roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) } - is EventSharedAction.Quote -> { + is EventSharedAction.Quote -> { roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, views.composerLayout.text.toString())) } - is EventSharedAction.Reply -> { + is EventSharedAction.Reply -> { roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, views.composerLayout.text.toString())) } - is EventSharedAction.CopyPermalink -> { + is EventSharedAction.CopyPermalink -> { val permalink = session.permalinkService().createPermalink(roomDetailArgs.roomId, action.eventId) copyToClipboard(requireContext(), permalink, false) showSnackWithMessage(getString(R.string.copied_to_clipboard)) } - is EventSharedAction.Resend -> { + is EventSharedAction.Resend -> { roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId)) } - is EventSharedAction.Remove -> { + is EventSharedAction.Remove -> { roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId)) } is EventSharedAction.Cancel -> { handleCancelSend(action) } - is EventSharedAction.ReportContentSpam -> { + is EventSharedAction.ReportContentSpam -> { roomDetailViewModel.handle(RoomDetailAction.ReportContent( action.eventId, action.senderId, "This message is spam", spam = true)) } @@ -1800,22 +1814,22 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.ReportContent( action.eventId, action.senderId, "This message is inappropriate", inappropriate = true)) } - is EventSharedAction.ReportContentCustom -> { + is EventSharedAction.ReportContentCustom -> { promptReasonToReportContent(action) } - is EventSharedAction.IgnoreUser -> { + is EventSharedAction.IgnoreUser -> { action.senderId?.let { askConfirmationToIgnoreUser(it) } } - is EventSharedAction.OnUrlClicked -> { + is EventSharedAction.OnUrlClicked -> { onUrlClicked(action.url, action.title) } - is EventSharedAction.OnUrlLongClicked -> { + is EventSharedAction.OnUrlLongClicked -> { onUrlLongClicked(action.url) } - is EventSharedAction.ReRequestKey -> { + is EventSharedAction.ReRequestKey -> { roomDetailViewModel.handle(RoomDetailAction.ReRequestKeys(action.eventId)) } - is EventSharedAction.UseKeyBackup -> { + is EventSharedAction.UseKeyBackup -> { context?.let { startActivity(KeysBackupRestoreActivity.intent(it)) } @@ -1955,10 +1969,10 @@ class RoomDetailFragment @Inject constructor( private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { when (type) { - AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(requireContext(), attachmentPhotoActivityResultLauncher) - AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) + AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(requireContext(), attachmentPhotoActivityResultLauncher) + AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentImageActivityResultLauncher) - AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(attachmentAudioActivityResultLauncher) + AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(attachmentAudioActivityResultLauncher) AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment) }.exhaustive 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 d6eeade97e..80cb82098b 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 @@ -70,6 +70,7 @@ import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.share.SharedData +import im.vector.app.features.spaces.SpacePreviewActivity import im.vector.app.features.terms.ReviewTermsActivity import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgsBuilder @@ -102,7 +103,7 @@ class DefaultNavigator @Inject constructor( startActivity(context, intent, buildTask) } - override fun switchToSpace(context: Context, spaceId: String, roomId: String?) { + override fun switchToSpace(context: Context, spaceId: String, roomId: String?, openShareSheet: Boolean) { if (sessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(spaceId) == null) { fatalError("Trying to open an unknown space $spaceId", vectorPreferences.failFast()) return @@ -115,10 +116,16 @@ class DefaultNavigator @Inject constructor( Timber.d("## Nav: Failed to switch to space $spaceId") } if (roomId != null) { - openRoom(context, roomId) + val args = RoomDetailArgs(roomId, eventId = null, openShareSpaceForId = spaceId.takeIf { openShareSheet }) + val intent = RoomDetailActivity.newIntent(context, args) + startActivity(context, intent, false) } } + override fun openSpacePreview(context: Context, spaceId: String) { + startActivity(context, SpacePreviewActivity.newIntent(context, spaceId), false) + } + override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) { val session = sessionHolder.getSafeActiveSession() ?: return val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) 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 fcd35be162..225a2b89ab 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 @@ -38,7 +38,9 @@ interface Navigator { fun openRoom(context: Context, roomId: String, eventId: String? = null, buildTask: Boolean = false) - fun switchToSpace(context: Context, spaceId: String, roomId: String?) + fun switchToSpace(context: Context, spaceId: String, roomId: String?, openShareSheet: Boolean) + + fun openSpacePreview(context: Context, spaceId: String) fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index ae6d630c75..d43337fab0 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.rx.rx @@ -147,33 +148,43 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti val membership = roomSummary?.membership val eventId = permalinkData.eventId val roomAlias = permalinkData.getRoomAliasOrNull() + val isSpace = roomSummary?.roomType == RoomType.SPACE return when { membership == Membership.BAN -> context.toast(R.string.error_opening_banned_room) membership?.isActive().orFalse() -> { - navigator.openRoom(context, roomId, eventId, buildTask) + if (isSpace) { +// navigator.switchToSpace(context, roomId, null) + navigator.openSpacePreview(context, roomId) + } else { + navigator.openRoom(context, roomId, eventId, buildTask) + } } else -> { - if (roomSummary == null) { - // we don't know this room, try to peek - val roomPreviewData = RoomPreviewData( - roomId = roomId, - roomAlias = roomAlias, - peekFromServer = true, - buildTask = buildTask, - homeServers = permalinkData.viaParameters - ) - navigator.openRoomPreview(context, roomPreviewData) + if (isSpace) { + navigator.openSpacePreview(context, roomId) } else { - val roomPreviewData = RoomPreviewData( - roomId = roomId, - eventId = eventId, - roomAlias = roomAlias ?: roomSummary.canonicalAlias, - roomName = roomSummary.displayName, - avatarUrl = roomSummary.avatarUrl, - buildTask = buildTask, - homeServers = permalinkData.viaParameters - ) - navigator.openRoomPreview(context, roomPreviewData) + if (roomSummary == null) { + // we don't know this room, try to peek + val roomPreviewData = RoomPreviewData( + roomId = roomId, + roomAlias = roomAlias, + peekFromServer = true, + buildTask = buildTask, + homeServers = permalinkData.viaParameters + ) + navigator.openRoomPreview(context, roomPreviewData) + } else { + val roomPreviewData = RoomPreviewData( + roomId = roomId, + eventId = eventId, + roomAlias = roomAlias ?: roomSummary.canonicalAlias, + roomName = roomSummary.displayName, + avatarUrl = roomSummary.avatarUrl, + buildTask = buildTask, + homeServers = permalinkData.viaParameters + ) + navigator.openRoomPreview(context, roomPreviewData) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt new file mode 100644 index 0000000000..d3fb225083 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt @@ -0,0 +1,107 @@ +/* + * 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.spaces + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import im.vector.app.R +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.core.utils.startSharePlainTextIntent +import im.vector.app.databinding.BottomSheetSpaceInviteBinding +import im.vector.app.features.invite.InviteUsersToRoomActivity +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment() { + + @Parcelize + data class Args( + val spaceId: String + ) : Parcelable + + override val showExpanded = true + + @Inject + lateinit var activeSessionHolder: ActiveSessionHolder + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSpaceInviteBinding { + return BottomSheetSpaceInviteBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Not going for full view model for now, as it may change + + val args: Args = arguments?.getParcelable(EXTRA_ARGS) + ?: return Unit.also { dismiss() } + val summary = activeSessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(args.spaceId)?.spaceSummary() + + val spaceName = summary?.roomSummary?.name + views.descriptionText.text = getString(R.string.invite_people_to_your_space_desc, spaceName) + + views.inviteByMailButton.debouncedClicks { + } + + views.inviteByMxidButton.debouncedClicks { + val intent = InviteUsersToRoomActivity.getIntent(requireContext(), args.spaceId) + startActivity(intent) + } + + views.inviteByLinkButton.debouncedClicks { + activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createRoomPermalink(args.spaceId)?.let { permalink -> + startSharePlainTextIntent( + fragment = this, + activityResultLauncher = null, + chooserTitle = getString(R.string.share_by_text), + text = getString(R.string.share_space_link_message, spaceName, permalink), + extraTitle = getString(R.string.share_space_link_message, spaceName, permalink) + ) + } + } + + views.skipButton.debouncedClicks { + dismiss() + } + } + + companion object { + + const val EXTRA_ARGS = "EXTRA_ARGS" + + fun show(fragmentManager: FragmentManager, spaceId: String): ShareSpaceBottomSheet { + return ShareSpaceBottomSheet().apply { + isCancelable = true + arguments = Bundle().apply { + this.putParcelable(EXTRA_ARGS, ShareSpaceBottomSheet.Args(spaceId = spaceId)) + } + }.also { + it.show(fragmentManager, ShareSpaceBottomSheet::class.java.name) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt index bc1af38473..f2ef3b84a0 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt @@ -80,6 +80,10 @@ class SpaceListFragment @Inject constructor( viewModel.handle(SpaceListAction.SelectSpace(spaceSummary)) } + override fun onLeaveSpace(spaceSummary: SpaceSummary) { + viewModel.handle(SpaceListAction.LeaveSpace(spaceSummary)) + } + override fun onAddSpaceSelected() { viewModel.handle(SpaceListAction.AddSpace) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt index d1bc3c6e1c..0402beb428 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt @@ -71,6 +71,7 @@ class SpaceSummaryController @Inject constructor( matrixItem(it.toMatrixItem()) selected(false) listener { callback?.onSpaceSelected(it) } +// lea { callback?.onSpaceSelected(it) } } } genericFooterItem { @@ -101,6 +102,7 @@ class SpaceSummaryController @Inject constructor( id(groupSummary.spaceId) matrixItem(groupSummary.toMatrixItem()) selected(isSelected) + onLeave { callback?.onLeaveSpace(groupSummary) } listener { callback?.onSpaceSelected(groupSummary) } } } @@ -118,6 +120,7 @@ class SpaceSummaryController @Inject constructor( interface Callback { fun onSpaceSelected(spaceSummary: SpaceSummary) + fun onLeaveSpace(spaceSummary: SpaceSummary) fun onAddSpaceSelected() } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt index bf3a47461f..3f1971d9dc 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt @@ -18,12 +18,14 @@ package im.vector.app.features.spaces import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.platform.CheckableConstraintLayout +import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem @@ -34,12 +36,22 @@ abstract class SpaceSummaryItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null + @EpoxyAttribute var onLeave: (() -> Unit)? = null override fun bind(holder: Holder) { super.bind(holder) holder.rootView.setOnClickListener { listener?.invoke() } holder.groupNameView.text = matrixItem.displayName holder.rootView.isChecked = selected + if (onLeave != null) { + holder.leaveView.setOnClickListener( + DebouncedClickListener({ _ -> + onLeave?.invoke() + }) + ) + } else { + holder.leaveView.isVisible = false + } avatarRenderer.renderSpace(matrixItem, holder.avatarImageView) } @@ -52,5 +64,6 @@ abstract class SpaceSummaryItem : VectorEpoxyModel() { val avatarImageView by bind(R.id.groupAvatarImageView) val groupNameView by bind(R.id.groupNameView) val rootView by bind(R.id.itemGroupLayout) + val leaveView by bind(R.id.groupTmpLeave) } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt index 4ec6e7d4ad..28bc358e36 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.spaces +import androidx.lifecycle.viewModelScope import arrow.core.Option import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext @@ -34,18 +35,22 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.grouplist.SelectedSpaceDataSource import io.reactivex.Observable import io.reactivex.functions.BiFunction +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.space.SpaceSummary +import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx const val ALL_COMMUNITIES_GROUP_ID = "+ALL_COMMUNITIES_GROUP_ID" sealed class SpaceListAction : VectorViewModelAction { data class SelectSpace(val spaceSummary: SpaceSummary) : SpaceListAction() + data class LeaveSpace(val spaceSummary: SpaceSummary) : SpaceListAction() object AddSpace : SpaceListAction() } @@ -88,13 +93,18 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp init { observeSpaceSummaries() observeSelectionState() - selectedSpaceDataSource.observe().execute { - if (this.selectedSpace != it.invoke()?.orNull()) { - copy( - selectedSpace = it.invoke()?.orNull() - ) - } else this - } + selectedSpaceDataSource + .observe() + .subscribe { + if (currentGroupId != it.orNull()?.spaceId) { + setState { + copy( + selectedSpace = it.orNull() + ) + } + } + } + .disposeOnClear() } private fun observeSelectionState() { @@ -119,11 +129,12 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp override fun handle(action: SpaceListAction) { when (action) { is SpaceListAction.SelectSpace -> handleSelectSpace(action) - else -> handleAddSpace() + is SpaceListAction.LeaveSpace -> handleLeaveSpace(action) + SpaceListAction.AddSpace -> handleAddSpace() } } - // PRIVATE METHODS ***************************************************************************** +// PRIVATE METHODS ***************************************************************************** private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state -> // get uptodate version of the space @@ -146,6 +157,16 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp } } + private fun handleLeaveSpace(action: SpaceListAction.LeaveSpace) { + viewModelScope.launch { + awaitCallback { + tryOrNull("Failed to leave space ${action.spaceSummary.spaceId}") { + session.spaceService().getSpace(action.spaceSummary.spaceId)?.asRoom()?.leave(null, it) + } + } + } + } + private fun handleAddSpace() { _viewEvents.post(SpaceListViewEvents.AddSpace) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt b/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt index 066c329c34..a6b9b1978a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt @@ -17,6 +17,7 @@ package im.vector.app.features.spaces.create import android.content.Context +import android.content.res.ColorStateList import android.graphics.drawable.Drawable import android.os.Build import android.util.AttributeSet @@ -25,6 +26,7 @@ import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.withStyledAttributes import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.databinding.ViewSpaceTypeButtonBinding class WizardButtonView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) @@ -36,7 +38,7 @@ class WizardButtonView @JvmOverloads constructor(context: Context, attrs: Attrib set(value) { if (value != title) { field = value - views.title.text = value + views.title.setTextOrHide(value) } } @@ -44,7 +46,7 @@ class WizardButtonView @JvmOverloads constructor(context: Context, attrs: Attrib set(value) { if (value != subTitle) { field = value - views.subTitle.text = value + views.subTitle.setTextOrHide(value) } } @@ -56,15 +58,13 @@ class WizardButtonView @JvmOverloads constructor(context: Context, attrs: Attrib } } -// private var tint: Int? = null -// set(value) { -// field = value -// if (value != null) { -// views.buttonImageView.imageTintList = ColorStateList.valueOf(value) -// } else { -// views.buttonImageView.clearColorFilter() -// } -// } + private var tint: Int? = null + set(value) { + field = value + if (value != null) { + views.buttonImageView.imageTintList = ColorStateList.valueOf(value) + } + } // var action: (() -> Unit)? = null @@ -72,16 +72,19 @@ class WizardButtonView @JvmOverloads constructor(context: Context, attrs: Attrib inflate(context, R.layout.view_space_type_button, this) views = ViewSpaceTypeButtonBinding.bind(this) + views.subTitle.setTextOrHide(null) + if (isInEditMode) { title = "Title" subTitle = "This is doing something" } context.withStyledAttributes(attrs, R.styleable.WizardButtonView) { - title = getString(R.styleable.WizardButtonView_title) ?: "" - subTitle = getString(R.styleable.WizardButtonView_subTitle) ?: "" + title = getString(R.styleable.WizardButtonView_title) + subTitle = getString(R.styleable.WizardButtonView_subTitle) icon = getDrawable(R.styleable.WizardButtonView_icon) -// tint = getColor(R.styleable.WizardButtonView_iconTint, ThemeUtils.getColor(context, R.attr.riotx_text_primary)) + tint = getColor(R.styleable.WizardButtonView_iconTint, -1) + .takeIf { it != -1 } } val outValue = TypedValue() diff --git a/vector/src/main/res/drawable/ic_mail.xml b/vector/src/main/res/drawable/ic_mail.xml new file mode 100644 index 0000000000..80d25166b0 --- /dev/null +++ b/vector/src/main/res/drawable/ic_mail.xml @@ -0,0 +1,21 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_share_link.xml b/vector/src/main/res/drawable/ic_share_link.xml new file mode 100644 index 0000000000..d4eceb3b9a --- /dev/null +++ b/vector/src/main/res/drawable/ic_share_link.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/layout/bottom_sheet_space_invite.xml b/vector/src/main/res/layout/bottom_sheet_space_invite.xml new file mode 100644 index 0000000000..c8d29b2f7d --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_space_invite.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_space.xml b/vector/src/main/res/layout/item_space.xml index 9cd07f3215..f52ef31ead 100644 --- a/vector/src/main/res/layout/item_space.xml +++ b/vector/src/main/res/layout/item_space.xml @@ -35,11 +35,26 @@ android:textSize="15sp" android:textStyle="bold" app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator" - app:layout_constraintEnd_toStartOf="@+id/groupAvatarChevron" + app:layout_constraintEnd_toStartOf="@+id/groupTmpLeave" app:layout_constraintStart_toEndOf="@+id/groupAvatarImageView" app:layout_constraintTop_toTopOf="parent" tools:text="@tools:sample/lorem/random" /> + + @@ -53,7 +54,8 @@ app:layout_constraintStart_toEndOf="@id/buttonImageView" app:layout_constraintTop_toBottomOf="@id/title" app:layout_goneMarginBottom="8dp" - tools:text="Open to anyone, best for communities" /> + tools:text="Open to anyone, best for communities" + tools:visibility="visible" /> We’ll create rooms for them, and auto-join everyone. You can add more later too. General Creating Space… + Invite people to your space + It’s just you at the moment. %s will be even better with others. + Invite by email + Invite by username + Share link + + Join my space %1$s %2$s + Skip for now