From 1fe0c8a3e98697d96f0016ae5568d2c816ca1832 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 28 May 2020 10:25:04 +0200 Subject: [PATCH] Widgets: handle actions (revoke, delete, edit, open in browser) and permissions bottom sheet --- .../android/api/query/QueryStringValue.kt | 4 +- .../integrationmanager/IntegrationManager.kt | 7 +- .../internal/session/widgets/Widget.kt | 10 +- .../internal/session/widgets/WidgetManager.kt | 16 +-- .../widgets/helper/UserAccountWidgets.kt | 10 +- .../session/widgets/helper/WidgetFactory.kt | 55 ++++++++ .../vector/riotx/core/di/ScreenComponent.kt | 5 + .../home/room/detail/RoomDetailFragment.kt | 2 +- .../features/navigation/DefaultNavigator.kt | 4 +- .../riotx/features/navigation/Navigator.kt | 2 +- .../riotx/features/widgets/WidgetAction.kt | 2 + .../riotx/features/widgets/WidgetActivity.kt | 87 ++++++++++-- .../features/widgets/WidgetArgsBuilder.kt | 13 +- .../riotx/features/widgets/WidgetFragment.kt | 89 +++++++++++-- .../features/widgets/WidgetViewEvents.kt | 8 +- .../riotx/features/widgets/WidgetViewModel.kt | 92 ++++++++++++- .../riotx/features/widgets/WidgetViewState.kt | 17 ++- .../RoomWidgetPermissionActions.kt | 24 ++++ .../RoomWidgetPermissionBottomSheet.kt | 126 ++++++++++++++++++ .../RoomWidgetPermissionViewModel.kt | 123 +++++++++++++++++ .../RoomWidgetPermissionViewState.kt | 44 ++++++ .../permissions/WidgetPermissionsHelper.kt | 41 ++++++ .../src/main/res/layout/activity_widget.xml | 20 +++ .../bottom_sheet_room_widget_permission.xml | 113 ++++++++++++++++ .../{menu_room_widget.xml => menu_widget.xml} | 12 +- 25 files changed, 850 insertions(+), 76 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/WidgetFactory.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionActions.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewState.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/widgets/permissions/WidgetPermissionsHelper.kt create mode 100755 vector/src/main/res/layout/activity_widget.xml create mode 100644 vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml rename vector/src/main/res/menu/{menu_room_widget.xml => menu_widget.xml} (66%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt index 5d3e76f1d3..ef99133ba6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt @@ -25,8 +25,8 @@ sealed class QueryStringValue { object IsNotNull : QueryStringValue() object IsEmpty : QueryStringValue() object IsNotEmpty : QueryStringValue() - data class Equals(val string: String, val case: Case) : QueryStringValue() - data class Contains(val string: String, val case: Case) : QueryStringValue() + data class Equals(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue() + data class Contains(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue() enum class Case { SENSITIVE, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt index 8b492ec58d..474b360605 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt @@ -33,7 +33,7 @@ import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAcco import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask -import im.vector.matrix.android.internal.session.widgets.Widget +import im.vector.matrix.android.internal.session.widgets.helper.WidgetFactory import im.vector.matrix.android.internal.session.widgets.helper.extractWidgetSequence import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -58,7 +58,8 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: private val stringProvider: StringProvider, private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val accountDataDataSource: AccountDataDataSource, - private val configExtractor: IntegrationManagerConfigExtractor) { + private val configExtractor: IntegrationManagerConfigExtractor, + private val widgetFactory: WidgetFactory) { private val currentConfigs = ArrayList() @@ -284,7 +285,7 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: } private fun UserAccountDataEvent.asIntegrationManagerWidgetContent(): WidgetContent? { - return extractWidgetSequence() + return extractWidgetSequence(widgetFactory) .filter { it.widgetContent.type == INTEGRATION_MANAGER_WIDGET } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/Widget.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/Widget.kt index 9c2c98f5c1..ca26183d8a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/Widget.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/Widget.kt @@ -17,14 +17,20 @@ package im.vector.matrix.android.internal.session.widgets import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.sender.SenderInfo import im.vector.matrix.android.api.session.widgets.model.WidgetContent data class Widget( val widgetContent: WidgetContent, - val event: Event? = null, - val widgetId: String? = null + val event: Event, + val widgetId: String, + val senderInfo: SenderInfo?, + val isAddedByMe: Boolean ) { val isActive = widgetContent.type != null && widgetContent.url != null + + val name = widgetContent.getHumanName() + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetManager.kt index 3c6c7d967d..517979e8c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/WidgetManager.kt @@ -31,7 +31,6 @@ import im.vector.matrix.android.api.session.integrationmanager.IntegrationManage import im.vector.matrix.android.api.session.room.model.PowerLevelsContent import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.matrix.android.api.session.widgets.WidgetService -import im.vector.matrix.android.api.session.widgets.model.WidgetContent import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.SessionScope @@ -40,6 +39,7 @@ import im.vector.matrix.android.internal.session.room.state.StateEventDataSource import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource +import im.vector.matrix.android.internal.session.widgets.helper.WidgetFactory import im.vector.matrix.android.internal.session.widgets.helper.extractWidgetSequence import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.launchToCallback @@ -52,6 +52,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager: private val stateEventDataSource: StateEventDataSource, private val taskExecutor: TaskExecutor, private val createWidgetTask: CreateWidgetTask, + private val widgetFactory: WidgetFactory, @UserId private val userId: String) : IntegrationManagerService.Listener { private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } @@ -104,19 +105,16 @@ internal class WidgetManager @Inject constructor(private val integrationManager: } // Create each widget from its latest im.vector.modular.widgets state event for (widgetEvent in sortedWidgetEvents) { // Filter widget types if required - val widgetContent = widgetEvent.content.toModel() - if (widgetContent?.url == null) continue - val widgetType = widgetContent.type ?: continue + val widget = widgetFactory.create(widgetEvent) ?: continue + val widgetType = widget.widgetContent.type ?: continue if (widgetTypes != null && !widgetTypes.contains(widgetType)) { continue } if (excludedTypes != null && excludedTypes.contains(widgetType)) { continue } - // widgetEvent.stateKey = widget id - if (widgetEvent.stateKey != null && !widgets.containsKey(widgetEvent.stateKey)) { - val widget = Widget(widgetContent, widgetEvent, widgetEvent.stateKey) - widgets[widgetEvent.stateKey] = widget + if (!widgets.containsKey(widget.widgetId)) { + widgets[widget.widgetId] = widget } } return widgets.values.toList() @@ -142,7 +140,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager: private fun UserAccountDataEvent.mapToWidgets(widgetTypes: Set? = null, excludedTypes: Set? = null): List { - return extractWidgetSequence() + return extractWidgetSequence(widgetFactory) .filter { val widgetType = it.widgetContent.type ?: return@filter false (widgetTypes == null || widgetTypes.contains(widgetType)) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/UserAccountWidgets.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/UserAccountWidgets.kt index 803413f7ce..e8ec46a9f9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/UserAccountWidgets.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/UserAccountWidgets.kt @@ -18,22 +18,16 @@ package im.vector.matrix.android.internal.session.widgets.helper import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.widgets.model.WidgetContent import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.internal.session.widgets.Widget -internal fun UserAccountDataEvent.extractWidgetSequence(): Sequence { +internal fun UserAccountDataEvent.extractWidgetSequence(widgetFactory: WidgetFactory): Sequence { return content.asSequence() .mapNotNull { @Suppress("UNCHECKED_CAST") (it.value as? JsonDict)?.toModel() }.mapNotNull { event -> - val content = event.content?.toModel() - if (content == null) { - null - } else { - Widget(content, event, event.stateKey) - } + widgetFactory.create(event) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/WidgetFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/WidgetFactory.kt new file mode 100644 index 0000000000..d3bf130f04 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/widgets/helper/WidgetFactory.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 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.matrix.android.internal.session.widgets.helper + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.sender.SenderInfo +import im.vector.matrix.android.api.session.widgets.model.WidgetContent +import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper +import im.vector.matrix.android.internal.session.widgets.Widget +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +internal class WidgetFactory @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, + @UserId private val userId: String) { + + fun create(widgetEvent: Event): Widget? { + val widgetContent = widgetEvent.content.toModel() + if (widgetContent?.url == null) return null + val widgetId = widgetEvent.stateKey ?: return null + val senderInfo = if (widgetEvent.senderId == null || widgetEvent.roomId == null) { + null + } else { + Realm.getInstance(realmConfiguration).use { + val roomMemberHelper = RoomMemberHelper(it, widgetEvent.roomId) + val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(widgetEvent.senderId) + SenderInfo( + userId = widgetEvent.senderId, + displayName = roomMemberSummaryEntity?.displayName, + isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName), + avatarUrl = roomMemberSummaryEntity?.avatarUrl + ) + } + } + val isAddedByMe = widgetEvent.senderId == userId + return Widget(widgetContent, widgetEvent, widgetId, senderInfo, isAddedByMe) + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 7d61c13b83..f7bca33d4f 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -63,6 +63,8 @@ import im.vector.riotx.features.share.IncomingShareActivity import im.vector.riotx.features.signout.soft.SoftLogoutActivity import im.vector.riotx.features.terms.ReviewTermsActivity import im.vector.riotx.features.ui.UiStateRepository +import im.vector.riotx.features.widgets.WidgetActivity +import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet @Component( dependencies = [ @@ -120,6 +122,7 @@ interface ScreenComponent { fun inject(activity: BigImageViewerActivity) fun inject(activity: InviteUsersToRoomActivity) fun inject(activity: ReviewTermsActivity) + fun inject(widgetActivity: WidgetActivity) /* ========================================================================================== * BottomSheets @@ -134,6 +137,7 @@ interface ScreenComponent { fun inject(bottomSheet: DeviceVerificationInfoBottomSheet) fun inject(bottomSheet: DeviceListBottomSheet) fun inject(bottomSheet: BootstrapBottomSheet) + fun inject(bottomSheet: RoomWidgetPermissionBottomSheet) /* ========================================================================================== * Others @@ -143,6 +147,7 @@ interface ScreenComponent { fun inject(preference: UserAvatarPreference) fun inject(button: ReactionButton) + /* ========================================================================================== * Factory * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 35033d170d..5f41daaeaf 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -329,7 +329,7 @@ class RoomDetailFragment @Inject constructor( context = requireContext(), roomId = roomDetailArgs.roomId, integId = null, - screenId = "type_${StickerPickerConstants.WIDGET_NAME}" + screen = StickerPickerConstants.WIDGET_NAME ) } .setNegativeButton(R.string.no, null) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index fa30bef33e..fc49564a8c 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -233,8 +233,8 @@ class DefaultNavigator @Inject constructor( fragment.startActivityForResult(intent, StickerPickerConstants.STICKER_PICKER_REQUEST_CODE) } - override fun openIntegrationManager(context: Context, roomId: String, integId: String?, screenId: String?) { - val widgetArgs = widgetArgsBuilder.buildIntegrationManagerArgs(roomId, integId, screenId) + override fun openIntegrationManager(context: Context, roomId: String, integId: String?, screen: String?) { + val widgetArgs = widgetArgsBuilder.buildIntegrationManagerArgs(roomId, integId, screen) context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index b0189919a1..c2798062e1 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -87,7 +87,7 @@ interface Navigator { widget: Widget, requestCode: Int = StickerPickerConstants.STICKER_PICKER_REQUEST_CODE) - fun openIntegrationManager(context: Context, roomId: String, integId: String?, screenId: String?) + fun openIntegrationManager(context: Context, roomId: String, integId: String?, screen: String?) fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetAction.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetAction.kt index 8984ef9210..af81d8eb0f 100644 --- a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetAction.kt @@ -22,5 +22,7 @@ sealed class WidgetAction : VectorViewModelAction { data class OnWebViewStartedToLoad(val url: String) : WidgetAction() data class OnWebViewLoadingError(val url: String, val isHttpError: Boolean, val errorCode: Int, val errorDescription: String) : WidgetAction() data class OnWebViewLoadingSuccess(val url: String) : WidgetAction() + object DeleteWidget: WidgetAction() + object RevokeWidget: WidgetAction() object OnTermsReviewed: WidgetAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetActivity.kt index 93356541ab..cee15b0a3b 100644 --- a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetActivity.kt @@ -16,26 +16,35 @@ package im.vector.riotx.features.widgets +import android.app.Activity import android.content.Context import android.content.Intent import androidx.appcompat.widget.Toolbar +import androidx.core.view.isVisible +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.viewModel import im.vector.matrix.android.api.session.events.model.Content import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet +import kotlinx.android.synthetic.main.activity_widget.* import java.io.Serializable +import javax.inject.Inject -class WidgetActivity : VectorBaseActivity(), ToolbarConfigurable { +class WidgetActivity : VectorBaseActivity(), ToolbarConfigurable, WidgetViewModel.Factory { companion object { + private const val WIDGET_FRAGMENT_TAG = "WIDGET_FRAGMENT_TAG" + private const val WIDGET_PERMISSION_FRAGMENT_TAG = "WIDGET_PERMISSION_FRAGMENT_TAG" private const val EXTRA_RESULT = "EXTRA_RESULT" - private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS" fun newIntent(context: Context, args: WidgetArgs): Intent { return Intent(context, WidgetActivity::class.java).apply { - putExtra(EXTRA_FRAGMENT_ARGS, args) + putExtra(MvRx.KEY_ARG, args) } } @@ -51,14 +60,76 @@ class WidgetActivity : VectorBaseActivity(), ToolbarConfigurable { } } - override fun getLayoutRes() = R.layout.activity_simple + @Inject lateinit var viewModelFactory: WidgetViewModel.Factory + private val viewModel: WidgetViewModel by viewModel() + + override fun getLayoutRes() = R.layout.activity_widget + + override fun getMenuRes() = R.menu.menu_widget + + override fun getTitleRes() = R.string.room_widget_activity_title + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } override fun initUiAndData() { - if (isFirstCreation()) { - val fragmentArgs: WidgetArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS) - ?: return - addFragment(R.id.simpleFragmentContainer, WidgetFragment::class.java, fragmentArgs) + val widgetArgs: WidgetArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) + ?: return + + configure(toolbar) + toolbar.isVisible = widgetArgs.kind.nameRes != 0 + viewModel.observeViewEvents { + when (it) { + is WidgetViewEvents.Close -> handleClose(it) + } } + + viewModel.selectSubscribe(this, WidgetViewState::status) { ws -> + when (ws) { + WidgetStatus.UNKNOWN -> { + } + WidgetStatus.WIDGET_NOT_ALLOWED -> { + val dFrag = supportFragmentManager.findFragmentByTag(WIDGET_PERMISSION_FRAGMENT_TAG) as? RoomWidgetPermissionBottomSheet + if (dFrag != null && dFrag.dialog?.isShowing == true && !dFrag.isRemoving) { + return@selectSubscribe + } else { + RoomWidgetPermissionBottomSheet + .newInstance(widgetArgs).apply { + onFinish = { accepted -> + if (!accepted) finish() + } + } + .show(supportFragmentManager, WIDGET_PERMISSION_FRAGMENT_TAG) + } + } + WidgetStatus.WIDGET_ALLOWED -> { + if (supportFragmentManager.findFragmentByTag(WIDGET_FRAGMENT_TAG) == null) { + addFragment(R.id.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG) + } + } + } + } + + viewModel.selectSubscribe(this, WidgetViewState::widgetName) { name -> + supportActionBar?.title = name + } + + viewModel.selectSubscribe(this, WidgetViewState::canManageWidgets) { + invalidateOptionsMenu() + } + } + + override fun create(initialState: WidgetViewState): WidgetViewModel { + return viewModelFactory.create(initialState) + } + + private fun handleClose(event: WidgetViewEvents.Close) { + if (event.content != null) { + val intent = createResultIntent(event.content) + setResult(Activity.RESULT_OK, intent) + } + finish() } override fun configure(toolbar: Toolbar) { diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetArgsBuilder.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetArgsBuilder.kt index d352dfd1ce..d56b02ed8d 100644 --- a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetArgsBuilder.kt +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetArgsBuilder.kt @@ -23,15 +23,20 @@ import javax.inject.Inject class WidgetArgsBuilder @Inject constructor(private val sessionHolder: ActiveSessionHolder) { @Suppress("UNCHECKED_CAST") - fun buildIntegrationManagerArgs(roomId: String, integId: String?, screenId: String?): WidgetArgs { + fun buildIntegrationManagerArgs(roomId: String, integId: String?, screen: String?): WidgetArgs { val session = sessionHolder.getActiveSession() val integrationManagerConfig = session.integrationManagerService().getPreferredConfig() + val normalizedScreen = when { + screen == null -> null + screen.startsWith("type_") -> screen + else -> "type_$screen" + } return WidgetArgs( baseUrl = integrationManagerConfig.uiUrl, kind = WidgetKind.INTEGRATION_MANAGER, roomId = roomId, urlParams = mapOf( - "screen" to screenId, + "screen" to normalizedScreen, "integ_id" to integId, "room_id" to roomId ).filterNotNull() @@ -44,7 +49,7 @@ class WidgetArgsBuilder @Inject constructor(private val sessionHolder: ActiveSes val baseUrl = widget.widgetContent.url ?: throw IllegalStateException() return WidgetArgs( baseUrl = baseUrl, - kind = WidgetKind.USER, + kind = WidgetKind.STICKER_PICKER, roomId = roomId, widgetId = widgetId, urlParams = mapOf( @@ -55,7 +60,7 @@ class WidgetArgsBuilder @Inject constructor(private val sessionHolder: ActiveSes } @Suppress("UNCHECKED_CAST") - private fun Map.filterNotNull(): Map{ + private fun Map.filterNotNull(): Map { return filterValues { it != null } as Map } } diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetFragment.kt index a1655ba805..1e824937ce 100644 --- a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetFragment.kt @@ -20,19 +20,25 @@ import android.app.Activity import android.content.Intent import android.os.Bundle import android.os.Parcelable +import android.view.Menu +import android.view.MenuItem import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.core.view.forEach import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.args -import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.session.terms.TermsService import im.vector.riotx.R +import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.features.terms.ReviewTermsActivity import im.vector.riotx.features.webview.WebViewEventListener import im.vector.riotx.features.widgets.webview.clearAfterWidget @@ -51,17 +57,16 @@ data class WidgetArgs( val urlParams: Map = emptyMap() ) : Parcelable -class WidgetFragment @Inject constructor( - private val viewModelFactory: WidgetViewModel.Factory -) : VectorBaseFragment(), WidgetViewModel.Factory by viewModelFactory, WebViewEventListener { +class WidgetFragment @Inject constructor() : VectorBaseFragment(), WebViewEventListener, OnBackPressed { private val fragmentArgs: WidgetArgs by args() - private val viewModel: WidgetViewModel by fragmentViewModel() + private val viewModel: WidgetViewModel by activityViewModel() override fun getLayoutResId() = R.layout.fragment_room_widget override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setHasOptionsMenu(true) widgetWebView.setupForWidget(this) if (fragmentArgs.kind.isAdmin()) { viewModel.getPostAPIMediator().setWebView(widgetWebView) @@ -70,7 +75,6 @@ class WidgetFragment @Inject constructor( when (it) { is WidgetViewEvents.DisplayTerms -> displayTerms(it) is WidgetViewEvents.LoadFormattedURL -> loadFormattedUrl(it) - is WidgetViewEvents.Close -> handleClose(it) is WidgetViewEvents.DisplayIntegrationManager -> displayIntegrationManager(it) } } @@ -110,6 +114,59 @@ class WidgetFragment @Inject constructor( } } + override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state -> + val widget = state.asyncWidget() + menu.findItem(R.id.action_edit)?.isVisible = state.widgetKind != WidgetKind.INTEGRATION_MANAGER + if (widget == null) { + menu.findItem(R.id.action_refresh)?.isVisible = false + menu.findItem(R.id.action_widget_open_ext)?.isVisible = false + menu.findItem(R.id.action_delete)?.isVisible = false + menu.findItem(R.id.action_revoke)?.isVisible = false + } else { + menu.findItem(R.id.action_refresh)?.isVisible = true + menu.findItem(R.id.action_widget_open_ext)?.isVisible = true + menu.findItem(R.id.action_delete)?.isVisible = state.canManageWidgets && widget.isAddedByMe + menu.findItem(R.id.action_revoke)?.isVisible = state.status == WidgetStatus.WIDGET_ALLOWED && !widget.isAddedByMe + } + super.onPrepareOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) { state -> + when (item.itemId) { + R.id.action_edit -> { + navigator.openIntegrationManager(requireContext(), state.roomId, state.widgetId, state.widgetKind.screenId) + return@withState true + } + R.id.action_delete -> { + viewModel.handle(WidgetAction.DeleteWidget) + return@withState true + } + R.id.action_refresh -> if (state.formattedURL.complete) { + widgetWebView.reload() + return@withState true + } + R.id.action_widget_open_ext -> if (state.formattedURL.complete) { + openUrlInExternalBrowser(requireContext(), state.formattedURL.invoke()) + return@withState true + } + R.id.action_revoke -> if (state.status == WidgetStatus.WIDGET_ALLOWED) { + viewModel.handle(WidgetAction.RevokeWidget) + return@withState true + } + } + return@withState super.onOptionsItemSelected(item) + } + + override fun onBackPressed(toolbarButton: Boolean): Boolean = withState(viewModel) { state -> + if (state.formattedURL.complete) { + if (widgetWebView.canGoBack()) { + widgetWebView.goBack() + return@withState true + } + } + return@withState false + } + override fun invalidate() = withState(viewModel) { state -> Timber.v("Invalidate state: $state") when (state.status) { @@ -211,15 +268,21 @@ class WidgetFragment @Inject constructor( context = vectorBaseActivity, roomId = fragmentArgs.roomId, integId = event.integId, - screenId = event.integType + screen = event.integType ) } - private fun handleClose(event: WidgetViewEvents.Close) { - if (event.content != null) { - val intent = WidgetActivity.createResultIntent(event.content) - vectorBaseActivity.setResult(Activity.RESULT_OK, intent) - } - vectorBaseActivity.finish() + fun deleteWidget() { + AlertDialog.Builder(requireContext()) + .setMessage(R.string.widget_delete_message_confirmation) + .setPositiveButton(R.string.remove) { _, _ -> + viewModel.handle(WidgetAction.DeleteWidget) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + fun revokeWidget() { + viewModel.handle(WidgetAction.RevokeWidget) } } diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewEvents.kt index 91fcb25d8b..76d1b0837c 100644 --- a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewEvents.kt @@ -20,8 +20,8 @@ import im.vector.matrix.android.api.session.events.model.Content import im.vector.riotx.core.platform.VectorViewEvents sealed class WidgetViewEvents : VectorViewEvents { - data class Close(val content: Content?): WidgetViewEvents() - data class DisplayIntegrationManager(val integId: String?, val integType: String?): WidgetViewEvents() - data class LoadFormattedURL(val formattedURL: String): WidgetViewEvents() - data class DisplayTerms(val url: String, val token: String): WidgetViewEvents() + data class Close(val content: Content? = null) : WidgetViewEvents() + data class DisplayIntegrationManager(val integId: String?, val integType: String?) : WidgetViewEvents() + data class LoadFormattedURL(val formattedURL: String) : WidgetViewEvents() + data class DisplayTerms(val url: String, val token: String) : WidgetViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewModel.kt index c6eed9b936..68b02ea3b1 100644 --- a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewModel.kt @@ -29,15 +29,26 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.matrix.android.internal.session.widgets.WidgetManagementFailure +import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.matrix.rx.mapOptional +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.unwrap import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.widgets.permissions.WidgetPermissionsHelper import kotlinx.coroutines.launch import timber.log.Timber import javax.net.ssl.HttpsURLConnection class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: WidgetViewState, private val widgetPostAPIHandlerFactory: WidgetPostAPIHandler.Factory, + private val stringProvider: StringProvider, private val session: Session) : VectorViewModel(initialState), WidgetPostAPIHandler.NavigationCallback, @@ -60,6 +71,7 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi } } + private val room = session.getRoom(initialState.roomId) private val widgetService = session.widgetService() private val integrationManagerService = session.integrationManagerService() private val widgetURLFormatter = widgetService.getWidgetURLFormatter() @@ -71,11 +83,57 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi val widgetPostAPIHandler = widgetPostAPIHandlerFactory.create(initialState.roomId, this) postAPIMediator.setHandler(widgetPostAPIHandler) } + setupName() refreshPermissionStatus() - observePermissionStatus() + subscribeToPermissionStatus() + observePowerLevel() + observeWidgetIfNeeded() + subscribeToWidget() } - private fun observePermissionStatus() { + private fun subscribeToWidget() { + asyncSubscribe(WidgetViewState::asyncWidget){ + setState { copy(widgetName = it.name) } + } + } + + private fun setupName() { + val nameRes = initialState.widgetKind.nameRes + if (nameRes != 0) { + val name = stringProvider.getString(nameRes) + setState { copy(widgetName = name) } + } + } + + private fun observePowerLevel() { + if (room == null) { + return + } + room.rx().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + .mapOptional { it.content.toModel() } + .unwrap() + .map { + PowerLevelsHelper(it).isAllowedToSend(true, session.myUserId) + }.subscribe { + setState { copy(canManageWidgets = it) } + }.disposeOnClear() + } + + private fun observeWidgetIfNeeded() { + if (initialState.widgetKind != WidgetKind.ROOM) { + return + } + val widgetId = initialState.widgetId ?: return + session.rx() + .liveRoomWidgets(initialState.roomId, QueryStringValue.Equals(widgetId)) + .filter { it.isNotEmpty() } + .map { it.first() } + .execute { + copy(asyncWidget = it) + } + } + + private fun subscribeToPermissionStatus() { selectSubscribe(WidgetViewState::status) { Timber.v("Widget status: $it") if (it == WidgetStatus.WIDGET_ALLOWED) { @@ -91,6 +149,26 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi is WidgetAction.OnWebViewLoadingError -> handleWebViewLoadingError(action.isHttpError, action.errorCode, action.errorDescription) is WidgetAction.OnWebViewLoadingSuccess -> handleWebViewLoadingSuccess(action.url) is WidgetAction.OnWebViewStartedToLoad -> handleWebViewStartLoading() + WidgetAction.DeleteWidget -> handleDeleteWidget() + WidgetAction.RevokeWidget -> handleRevokeWidget() + WidgetAction.OnTermsReviewed -> refreshPermissionStatus() + } + } + + private fun handleRevokeWidget() { + viewModelScope.launch { + val widgetId = initialState.widgetId ?: return@launch + WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(initialState.roomId, widgetId, false) + _viewEvents.post(WidgetViewEvents.Close()) + } + } + + private fun handleDeleteWidget() { + viewModelScope.launch { + val widgetId = initialState.widgetId ?: return@launch + awaitCallback { + widgetService.destroyRoomWidget(initialState.roomId, widgetId, it) + } } } @@ -108,10 +186,10 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED) return } - if (roomWidget.event?.senderId == session.myUserId) { + if (roomWidget.event.senderId == session.myUserId) { setWidgetStatus(WidgetStatus.WIDGET_ALLOWED) } else { - val stateEventId = roomWidget.event?.eventId + val stateEventId = roomWidget.event.eventId // This should not happen if (stateEventId == null) { setWidgetStatus(WidgetStatus.WIDGET_NOT_ALLOWED) @@ -177,18 +255,18 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi } override fun onCleared() { - super.onCleared() integrationManagerService.removeListener(this) postAPIMediator.setHandler(null) + super.onCleared() } - // IntegrationManagerService.Listener +// IntegrationManagerService.Listener override fun onWidgetPermissionsChanged(widgets: Map) { refreshPermissionStatus() } - // WidgetPostAPIHandler.NavigationCallback +// WidgetPostAPIHandler.NavigationCallback override fun close() { _viewEvents.post(WidgetViewEvents.Close(null)) diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewState.kt b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewState.kt index f33bcab93e..fb211dd400 100644 --- a/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/widgets/WidgetViewState.kt @@ -16,9 +16,13 @@ package im.vector.riotx.features.widgets +import androidx.annotation.StringRes import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.internal.session.widgets.Widget +import im.vector.riotx.R +import im.vector.riotx.features.home.room.detail.sticker.StickerPickerConstants enum class WidgetStatus { UNKNOWN, @@ -26,14 +30,15 @@ enum class WidgetStatus { WIDGET_ALLOWED } -enum class WidgetKind { - ROOM, - USER, - INTEGRATION_MANAGER; +enum class WidgetKind(@StringRes val nameRes: Int, val screenId: String?) { + ROOM(R.string.room_widget_activity_title,null), + STICKER_PICKER(R.string.title_activity_choose_sticker, StickerPickerConstants.WIDGET_NAME), + INTEGRATION_MANAGER(0, null); fun isAdmin(): Boolean { - return this == USER || this == INTEGRATION_MANAGER + return this == STICKER_PICKER || this == INTEGRATION_MANAGER } + } data class WidgetViewState( @@ -47,7 +52,7 @@ data class WidgetViewState( val webviewLoadedUrl: Async = Uninitialized, val widgetName: String = "", val canManageWidgets: Boolean = false, - val createdByMe: Boolean = false + val asyncWidget: Async = Uninitialized ) : MvRxState { constructor(widgetArgs: WidgetArgs) : this( diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionActions.kt b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionActions.kt new file mode 100644 index 0000000000..3fb3c73da5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionActions.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 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.riotx.features.widgets.permissions + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class RoomWidgetPermissionActions : VectorViewModelAction { + object AllowWidget: RoomWidgetPermissionActions() + object BlockWidget: RoomWidgetPermissionActions() +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt new file mode 100644 index 0000000000..01c149b2f0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020 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.riotx.features.widgets.permissions + +import android.os.Build +import android.os.Parcelable +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.BulletSpan +import android.widget.ImageView +import android.widget.TextView +import butterknife.BindView +import butterknife.OnClick +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.withArgs +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.widgets.WidgetArgs +import kotlinx.android.parcel.Parcelize +import javax.inject.Inject + +class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() { + + override fun getLayoutResId(): Int = R.layout.bottom_sheet_room_widget_permission + + private val viewModel: RoomWidgetPermissionViewModel by fragmentViewModel() + + @BindView(R.id.bottom_sheet_widget_permission_shared_info) + lateinit var sharedInfoTextView: TextView + + @BindView(R.id.bottom_sheet_widget_permission_owner_id) + lateinit var authorIdText: TextView + + @BindView(R.id.bottom_sheet_widget_permission_owner_display_name) + lateinit var authorNameText: TextView + + @BindView(R.id.bottom_sheet_widget_permission_owner_avatar) + lateinit var authorAvatarView: ImageView + + @Inject lateinit var avatarRenderer: AvatarRenderer + + var onFinish: ((Boolean) -> Unit)? = null + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun invalidate() = withState(viewModel) { state -> + super.invalidate() + val permissionData = state.permissionData() ?: return@withState + authorIdText.text = permissionData.widget.senderInfo?.userId ?: "" + authorNameText.text = permissionData.widget.senderInfo?.disambiguatedDisplayName + permissionData.widget.senderInfo?.toMatrixItem()?.also { + avatarRenderer.render(it, authorAvatarView) + } + + val domain = permissionData.widgetDomain ?: "" + val infoBuilder = SpannableStringBuilder() + .append(getString( + R.string.room_widget_permission_webview_shared_info_title + .takeIf { permissionData.isWebviewWidget } + ?: R.string.room_widget_permission_shared_info_title, + "'$domain'")) + infoBuilder.append("\n") + permissionData.permissionsList.forEach { + infoBuilder.append("\n") + val bulletPoint = getString(it) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + infoBuilder.append(bulletPoint, BulletSpan(resources.getDimension(R.dimen.quote_gap).toInt()), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } else { + val start = infoBuilder.length + infoBuilder.append(bulletPoint) + infoBuilder.setSpan( + BulletSpan(resources.getDimension(R.dimen.quote_gap).toInt()), + start, + bulletPoint.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + infoBuilder.append("\n") + + sharedInfoTextView.text = infoBuilder + } + + @OnClick(R.id.bottom_sheet_widget_permission_decline_button) + fun doDecline() { + viewModel.handle(RoomWidgetPermissionActions.BlockWidget) + //optimistic dismiss + dismiss() + onFinish?.invoke(false) + } + + @OnClick(R.id.bottom_sheet_widget_permission_continue_button) + fun doAccept() { + viewModel.handle(RoomWidgetPermissionActions.AllowWidget) + //optimistic dismiss + dismiss() + onFinish?.invoke(true) + } + + companion object { + + fun newInstance(widgetArgs: WidgetArgs) = RoomWidgetPermissionBottomSheet().withArgs { + putParcelable(MvRx.KEY_ARG, widgetArgs) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewModel.kt new file mode 100644 index 0000000000..4e9b65f804 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewModel.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2020 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.riotx.features.widgets.permissions + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.extensions.orFalse +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.rx.rx +import im.vector.riotx.R +import im.vector.riotx.core.platform.EmptyViewEvents +import im.vector.riotx.core.platform.VectorViewModel +import kotlinx.coroutines.launch +import java.net.URL + +class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val initialState: RoomWidgetPermissionViewState, + private val session: Session) + : VectorViewModel(initialState) { + + private val widgetService = session.widgetService() + private val integrationManagerService = session.integrationManagerService() + + init { + observeWidget() + } + + private fun observeWidget() { + val widgetId = initialState.widgetId + session.rx() + .liveRoomWidgets(initialState.roomId, QueryStringValue.Equals(widgetId)) + .filter { it.isNotEmpty() } + .map { + val widget = it.first() + val domain = try { + URL(widget.widgetContent.url).host + } catch (e: Throwable) { + null + } + //TODO check from widget urls the perms that should be shown? + //For now put all + val infoShared = listOf( + R.string.room_widget_permission_display_name, + R.string.room_widget_permission_avatar_url, + R.string.room_widget_permission_user_id, + R.string.room_widget_permission_theme, + R.string.room_widget_permission_widget_id, + R.string.room_widget_permission_room_id + ) + RoomWidgetPermissionViewState.WidgetPermissionData( + widget = widget, + isWebviewWidget = true, + permissionsList = infoShared, + widgetDomain = domain + ) + } + .execute { + copy(permissionData = it) + } + } + + override fun handle(action: RoomWidgetPermissionActions) { + when (action) { + RoomWidgetPermissionActions.AllowWidget -> handleAllowWidget() + RoomWidgetPermissionActions.BlockWidget -> handleRevokeWidget() + } + } + + private fun handleRevokeWidget() = withState { state -> + viewModelScope.launch { + if (state.permissionData()?.isWebviewWidget.orFalse()) { + WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, state.widgetId, false) + } else { + //TODO JITSI + } + } + } + + private fun handleAllowWidget() = withState { state -> + viewModelScope.launch { + if (state.permissionData()?.isWebviewWidget.orFalse()) { + WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, state.widgetId, true) + } else { + //TODO JITSI + } + } + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewState.kt b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewState.kt new file mode 100644 index 0000000000..0b3a61b0f8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/RoomWidgetPermissionViewState.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 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.riotx.features.widgets.permissions + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.internal.session.widgets.Widget +import im.vector.riotx.features.widgets.WidgetArgs + +data class RoomWidgetPermissionViewState( + val roomId: String, + val widgetId: String, + val permissionData: Async = Uninitialized +) : MvRxState { + + constructor(widgetArgs: WidgetArgs) : this( + roomId = widgetArgs.roomId, + widgetId = widgetArgs.widgetId!! + ) + + data class WidgetPermissionData( + val widget: Widget, + val permissionsList: List = emptyList(), + val isWebviewWidget: Boolean = true, + val widgetDomain: String? = null + ) +} + + diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/permissions/WidgetPermissionsHelper.kt b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/WidgetPermissionsHelper.kt new file mode 100644 index 0000000000..3d192098ad --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/widgets/permissions/WidgetPermissionsHelper.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 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.riotx.features.widgets.permissions + +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService +import im.vector.matrix.android.api.session.widgets.WidgetService +import im.vector.matrix.android.internal.util.awaitCallback +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class WidgetPermissionsHelper(private val integrationManagerService: IntegrationManagerService, + private val widgetService: WidgetService) { + + suspend fun changePermission(roomId: String, widgetId: String, allow: Boolean) { + val widget = withContext(Dispatchers.Default) { + widgetService.getRoomWidgets( + roomId = roomId, + widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.SENSITIVE) + ).firstOrNull() + } + val eventId = widget?.event?.eventId ?: return + awaitCallback { + integrationManagerService.setWidgetAllowed(eventId, allow, it) + } + } +} diff --git a/vector/src/main/res/layout/activity_widget.xml b/vector/src/main/res/layout/activity_widget.xml new file mode 100755 index 0000000000..047bcbbc7c --- /dev/null +++ b/vector/src/main/res/layout/activity_widget.xml @@ -0,0 +1,20 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml b/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml new file mode 100644 index 0000000000..2cb6c2fd4c --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_room_widget_permission.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + +