Widgets: handle actions (revoke, delete, edit, open in browser) and permissions bottom sheet

This commit is contained in:
ganfra 2020-05-28 10:25:04 +02:00
parent e32716aa48
commit 1fe0c8a3e9
25 changed files with 850 additions and 76 deletions

View file

@ -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,

View file

@ -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
}

View file

@ -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()
}

View file

@ -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))

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
* ========================================================================================== */

View file

@ -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)

View file

@ -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))
}

View file

@ -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)?)

View file

@ -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()
}

View file

@ -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) {

View file

@ -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>
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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))

View file

@ -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(

View file

@ -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()
}

View file

@ -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)
}
}
}

View file

@ -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")
}
}
}

View file

@ -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
)
}

View file

@ -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)
}
}
}

View 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>

View file

@ -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>

View file

@ -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" />