mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-09 08:38:43 +01:00
Widgets: handle actions (revoke, delete, edit, open in browser) and permissions bottom sheet
This commit is contained in:
parent
e32716aa48
commit
1fe0c8a3e9
@ -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,
|
||||
|
@ -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<IntegrationManagerConfig>()
|
||||
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
}
|
||||
|
||||
|
@ -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<WidgetContent>()
|
||||
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<String>? = null,
|
||||
excludedTypes: Set<String>? = null): List<Widget> {
|
||||
return extractWidgetSequence()
|
||||
return extractWidgetSequence(widgetFactory)
|
||||
.filter {
|
||||
val widgetType = it.widgetContent.type ?: return@filter false
|
||||
(widgetTypes == null || widgetTypes.contains(widgetType))
|
||||
|
@ -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<Widget> {
|
||||
internal fun UserAccountDataEvent.extractWidgetSequence(widgetFactory: WidgetFactory): Sequence<Widget> {
|
||||
return content.asSequence()
|
||||
.mapNotNull {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(it.value as? JsonDict)?.toModel<Event>()
|
||||
}.mapNotNull { event ->
|
||||
val content = event.content?.toModel<WidgetContent>()
|
||||
if (content == null) {
|
||||
null
|
||||
} else {
|
||||
Widget(content, event, event.stateKey)
|
||||
}
|
||||
widgetFactory.create(event)
|
||||
}
|
||||
}
|
||||
|
@ -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<WidgetContent>()
|
||||
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)
|
||||
}
|
||||
}
|
@ -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
|
||||
* ========================================================================================== */
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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<Pair<View, String>>) -> Unit)?)
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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<String, String?>.filterNotNull(): Map<String, String>{
|
||||
private fun Map<String, String?>.filterNotNull(): Map<String, String> {
|
||||
return filterValues { it != null } as Map<String, String>
|
||||
}
|
||||
}
|
||||
|
@ -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<String, String> = 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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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<WidgetViewState, WidgetAction, WidgetViewEvents>(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<PowerLevelsContent>() }
|
||||
.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<Unit> {
|
||||
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<String, Boolean>) {
|
||||
refreshPermissionStatus()
|
||||
}
|
||||
|
||||
// WidgetPostAPIHandler.NavigationCallback
|
||||
// WidgetPostAPIHandler.NavigationCallback
|
||||
|
||||
override fun close() {
|
||||
_viewEvents.post(WidgetViewEvents.Close(null))
|
||||
|
@ -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<String> = Uninitialized,
|
||||
val widgetName: String = "",
|
||||
val canManageWidgets: Boolean = false,
|
||||
val createdByMe: Boolean = false
|
||||
val asyncWidget: Async<Widget> = Uninitialized
|
||||
) : MvRxState {
|
||||
|
||||
constructor(widgetArgs: WidgetArgs) : this(
|
||||
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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<RoomWidgetPermissionViewState, RoomWidgetPermissionActions, EmptyViewEvents>(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<RoomWidgetPermissionViewModel, RoomWidgetPermissionViewState> {
|
||||
|
||||
@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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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<WidgetPermissionData> = Uninitialized
|
||||
) : MvRxState {
|
||||
|
||||
constructor(widgetArgs: WidgetArgs) : this(
|
||||
roomId = widgetArgs.roomId,
|
||||
widgetId = widgetArgs.widgetId!!
|
||||
)
|
||||
|
||||
data class WidgetPermissionData(
|
||||
val widget: Widget,
|
||||
val permissionsList: List<Int> = emptyList(),
|
||||
val isWebviewWidget: Boolean = true,
|
||||
val widgetDomain: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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<Unit> {
|
||||
integrationManagerService.setWidgetAllowed(eventId, allow, it)
|
||||
}
|
||||
}
|
||||
}
|
20
vector/src/main/res/layout/activity_widget.xml
Executable file
20
vector/src/main/res/layout/activity_widget.xml
Executable file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/VectorToolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/root_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/room_widget_permission_title"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_widget_permission_h2_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/room_widget_permission_added_by"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginTop="@dimen/layout_vertical_margin"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<im.vector.view.VectorCircularImageView
|
||||
android:id="@+id/bottom_sheet_widget_permission_owner_avatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_widget_permission_owner_display_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="User name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_widget_permission_owner_id"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:textSize="14sp"
|
||||
tools:text="\@foo:matrix.org" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_widget_permission_shared_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginTop="@dimen/layout_vertical_margin_big"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="16sp"
|
||||
tools:text="@string/room_widget_permission_shared_info_title" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/layout_vertical_margin_big"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/bottom_sheet_widget_permission_decline_button"
|
||||
style="@style/VectorButtonStyleDestructive"
|
||||
android:layout_marginEnd="@dimen/layout_vertical_margin"
|
||||
android:layout_marginRight="@dimen/layout_vertical_margin"
|
||||
android:text="@string/decline"
|
||||
android:textAllCaps="true"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/bottom_sheet_widget_permission_continue_button"
|
||||
style="@style/VectorButtonStylePositive"
|
||||
android:layout_marginEnd="@dimen/layout_vertical_margin"
|
||||
android:layout_marginRight="@dimen/layout_vertical_margin"
|
||||
android:minWidth="160dp"
|
||||
android:text="@string/_continue"
|
||||
android:textAllCaps="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
@ -2,23 +2,23 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_edit"
|
||||
android:title="@string/edit"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_refresh"
|
||||
android:icon="@drawable/ic_refresh_cw"
|
||||
android:iconTint="?attr/vctr_icon_tint_on_dark_action_bar_color"
|
||||
android:title="@string/room_widget_reload"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_widget_open_ext"
|
||||
android:iconTint="?attr/vctr_icon_tint_on_dark_action_bar_color"
|
||||
android:title="@string/room_widget_open_in_browser"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_close"
|
||||
android:icon="@drawable/ic_close_round"
|
||||
android:iconTint="@color/vector_error_color"
|
||||
android:id="@+id/action_delete"
|
||||
android:title="@string/delete"
|
||||
app:showAsAction="never" />
|
||||
|
Loading…
x
Reference in New Issue
Block a user