Widget: continue working on interaction with SDK. Not sure yet how to manage properly distinction between room and "admin" widgets.

This commit is contained in:
ganfra 2020-05-20 20:39:18 +02:00
parent 3faf42be53
commit 00fd067c6b
38 changed files with 992 additions and 76 deletions

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.identity.IdentityService
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService
import im.vector.matrix.android.api.session.profile.ProfileService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
@ -65,8 +66,7 @@ interface Session :
HomeServerCapabilitiesService,
SecureStorageService,
AccountDataService,
AccountService,
WidgetService {
AccountService {
/**
* The params associated to the session
@ -155,6 +155,16 @@ interface Session :
*/
fun identityService(): IdentityService
/**
* Returns the widget service associated with the session
*/
fun widgetService(): WidgetService
/**
* Returns the integration manager service associated with the session
*/
fun integrationManagerService(): IntegrationManagerService
/**
* Add a listener to the session.
* @param listener the listener to add.

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.integrationmanager
package im.vector.matrix.android.api.session.integrationmanager
data class IntegrationManagerConfig(
val uiUrl: String,

View File

@ -0,0 +1,39 @@
/*
* 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.api.session.integrationmanager
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
interface IntegrationManagerService {
fun getOrderedConfigs(): List<IntegrationManagerConfig>
fun getPreferredConfig(): IntegrationManagerConfig
fun isIntegrationEnabled(): Boolean
fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback<Unit>): Cancelable
fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable
fun isWidgetAllowed(stateEventId: String): Boolean
fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable
fun isNativeWidgetAllowed(widgetType: String, domain: String?): Boolean
}

View File

@ -48,11 +48,30 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
return if (eventType.isNotEmpty() && userId.isNotEmpty()) {
val powerLevel = getUserPowerLevel(userId)
val minimumPowerLevel = powerLevelsContent.events[eventType]
?: if (EventType.isStateEvent(eventType)) {
powerLevelsContent.stateDefault
} else {
powerLevelsContent.eventsDefault
}
?: if (EventType.isStateEvent(eventType)) {
powerLevelsContent.stateDefault
} else {
powerLevelsContent.eventsDefault
}
powerLevel >= minimumPowerLevel
} else false
}
/**
* Tell if an user can send an event of a certain type
*
* @param eventType the event type to check for
* @param userId the user id
* @return true if the user can send this type of event
*/
fun isAllowedToSend(isState: Boolean, userId: String): Boolean {
return if (userId.isNotEmpty()) {
val powerLevel = getUserPowerLevel(userId)
val minimumPowerLevel = if (isState) {
powerLevelsContent.stateDefault
} else {
powerLevelsContent.eventsDefault
}
powerLevel >= minimumPowerLevel
} else false
}

View File

@ -20,6 +20,8 @@ import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional
interface StateService {
@ -27,7 +29,9 @@ interface StateService {
/**
* Update the topic of the room
*/
fun updateTopic(topic: String, callback: MatrixCallback<Unit>)
fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?

View File

@ -58,10 +58,11 @@ interface WidgetPostAPIMediator {
/**
* Send an object response
*
* @param klass the class of the response
* @param response the response
* @param eventData the modular data
*/
fun sendObjectResponse(response: JsonDict?, eventData: JsonDict)
fun <T> sendObjectResponse(klass: Class<T>, response: T?, eventData: JsonDict)
/**
* Send success
@ -82,6 +83,6 @@ interface WidgetPostAPIMediator {
/**
* Triggered when a widget is posting
*/
fun handleWidgetRequest(data: JsonDict): Boolean
fun handleWidgetRequest(eventData: JsonDict): Boolean
}
}

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.api.session.widgets
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Content
@ -35,6 +36,23 @@ interface WidgetService {
excludedTypes: Set<String>? = null
): List<Widget>
fun getRoomWidgetsLive(
roomId: String,
widgetId: QueryStringValue = QueryStringValue.NoCondition,
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): LiveData<List<Widget>>
fun getUserWidgets(
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): List<Widget>
fun getUserWidgetsLive(
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): LiveData<List<Widget>>
fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback<Widget>): Cancelable
fun destroyRoomWidget(roomId: String, widgetId: String, callback: MatrixCallback<Unit>): Cancelable

View File

@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService
import im.vector.matrix.android.api.session.profile.ProfileService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
@ -46,7 +47,6 @@ import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.widgets.WidgetService
import im.vector.matrix.android.api.session.widgets.WidgetURLBuilder
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater
@ -54,13 +54,11 @@ import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import im.vector.matrix.android.internal.session.widgets.WidgetDependenciesHolder
import im.vector.matrix.android.internal.session.widgets.WidgetManager
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.Dispatchers
@ -109,6 +107,7 @@ internal class DefaultSession @Inject constructor(
private val timelineEventDecryptor: TimelineEventDecryptor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val defaultIdentityService: DefaultIdentityService,
private val integrationManagerService: IntegrationManagerService,
private val taskExecutor: TaskExecutor,
private val widgetDependenciesHolder: WidgetDependenciesHolder,
private val shieldTrustUpdater: ShieldTrustUpdater)
@ -128,8 +127,7 @@ internal class DefaultSession @Inject constructor(
HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),
ProfileService by profileService.get(),
AccountDataService by accountDataService.get(),
AccountService by accountService.get(),
WidgetService by widgetService.get() {
AccountService by accountService.get() {
override val sharedSecretStorageService: SharedSecretStorageService
get() = _sharedSecretStorageService.get()
@ -243,6 +241,10 @@ internal class DefaultSession @Inject constructor(
override fun identityService() = defaultIdentityService
override fun widgetService(): WidgetService = widgetService.get()
override fun integrationManagerService() = integrationManagerService
override fun addListener(listener: Session.Listener) {
sessionListeners.addListener(listener)
}

View File

@ -37,6 +37,7 @@ import im.vector.matrix.android.internal.session.group.GetGroupDataWorker
import im.vector.matrix.android.internal.session.group.GroupModule
import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule
import im.vector.matrix.android.internal.session.identity.IdentityModule
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManagerModule
import im.vector.matrix.android.internal.session.openid.OpenIdModule
import im.vector.matrix.android.internal.session.profile.ProfileModule
import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker
@ -76,6 +77,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
PushersModule::class,
OpenIdModule::class,
WidgetModule::class,
IntegrationManagerModule::class,
IdentityModule::class,
TermsModule::class,
AccountDataModule::class,

View File

@ -0,0 +1,58 @@
/*
* 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.integrationmanager
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService
import im.vector.matrix.android.api.util.Cancelable
import javax.inject.Inject
internal class DefaultIntegrationManagerService @Inject constructor(private val integrationManager: IntegrationManager) : IntegrationManagerService {
override fun getOrderedConfigs(): List<IntegrationManagerConfig> {
return integrationManager.getOrderedConfigs()
}
override fun getPreferredConfig(): IntegrationManagerConfig {
return integrationManager.getPreferredConfig()
}
override fun isIntegrationEnabled(): Boolean {
return integrationManager.isIntegrationEnabled()
}
override fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback<Unit>): Cancelable {
return integrationManager.setIntegrationEnabled(enable, callback)
}
override fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable {
return integrationManager.setWidgetAllowed(stateEventId, allowed, callback)
}
override fun isWidgetAllowed(stateEventId: String): Boolean {
return integrationManager.isWidgetAllowed(stateEventId)
}
override fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable {
return integrationManager.setNativeWidgetDomainAllowed(widgetType, domain, allowed, callback)
}
override fun isNativeWidgetAllowed(widgetType: String, domain: String?): Boolean {
return integrationManager.isNativeWidgetAllowed(widgetType, domain)
}
}

View File

@ -22,6 +22,7 @@ import androidx.lifecycle.LifecycleRegistry
import im.vector.matrix.android.R
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig
import im.vector.matrix.android.api.session.widgets.model.WidgetContent
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
@ -31,7 +32,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.helper.extractWidgets
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
import im.vector.matrix.android.internal.util.StringProvider
@ -294,7 +295,7 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor:
}
private fun UserAccountDataEvent.asIntegrationManagerWidgetContent(): WidgetContent? {
return extractWidgets()
return extractWidgetSequence()
.filter {
it.type == INTEGRATION_MANAGER_WIDGET
}

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.integrationmanager
import im.vector.matrix.android.api.auth.data.WellKnown
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig
import javax.inject.Inject
internal class IntegrationManagerConfigExtractor @Inject constructor() {

View File

@ -0,0 +1,28 @@
/*
* 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.integrationmanager
import dagger.Binds
import dagger.Module
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService
@Module
internal abstract class IntegrationManagerModule {
@Binds
abstract fun bindIntegrationManagerService(integrationManagerService: DefaultIntegrationManagerService): IntegrationManagerService
}

View File

@ -24,6 +24,8 @@ import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
@ -55,17 +57,29 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey)
}
override fun updateTopic(topic: String, callback: MatrixCallback<Unit>) {
val params = SendStateTask.Params(roomId,
EventType.STATE_ROOM_TOPIC,
mapOf(
"topic" to topic
))
sendStateTask
override fun sendStateEvent(
eventType: String,
stateKey: String?,
body: JsonDict,
callback: MatrixCallback<Unit>
): Cancelable {
val params = SendStateTask.Params(
roomId = roomId,
eventType = eventType,
body = body
)
return sendStateTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable {
return sendStateEvent(
eventType = EventType.STATE_ROOM_TOPIC,
body = mapOf("topic" to topic),
callback = callback
)
}
}

View File

@ -26,6 +26,7 @@ import javax.inject.Inject
internal interface SendStateTask : Task<SendStateTask.Params, Unit> {
data class Params(
val roomId: String,
val stateKey: String? = null,
val eventType: String,
val body: JsonDict
)
@ -38,7 +39,20 @@ internal class DefaultSendStateTask @Inject constructor(
override suspend fun execute(params: SendStateTask.Params) {
return executeRequest(eventBus) {
apiCall = roomAPI.sendStateEvent(params.roomId, params.eventType, params.body)
apiCall = if (params.stateKey == null) {
roomAPI.sendStateEvent(
roomId = params.roomId,
stateEventType = params.eventType,
params = params.body
)
} else {
roomAPI.sendStateEvent(
roomId = params.roomId,
stateEventType = params.eventType,
stateKey = params.stateKey,
params = params.body
)
}
}
}
}

View File

@ -27,10 +27,10 @@ import timber.log.Timber
import java.util.HashMap
import javax.inject.Inject
internal class DefaultWidgetPostAPIMediator @Inject constructor(moshi: Moshi,
internal class DefaultWidgetPostAPIMediator @Inject constructor(private val moshi: Moshi,
private val widgetPostMessageAPIProvider: WidgetPostMessageAPIProvider) : WidgetPostAPIMediator {
private val adapter = moshi.adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
private val jsonAdapter = moshi.adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
private var handler: WidgetPostAPIMediator.Handler? = null
private var webView: WebView? = null
@ -58,7 +58,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(moshi: Moshi,
fun onWidgetEvent(jsonEventData: String) {
Timber.d("BRIDGE onWidgetEvent : $jsonEventData")
try {
val dataAsDict = adapter.fromJson(jsonEventData)
val dataAsDict = jsonAdapter.fromJson(jsonEventData)
@Suppress("UNCHECKED_CAST")
val eventData = (dataAsDict?.get("event.data") as? JsonDict) ?: return
onWidgetMessage(eventData)
@ -111,11 +111,12 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(moshi: Moshi,
* @param response the response
* @param eventData the modular data
*/
override fun sendObjectResponse(response: JsonDict?, eventData: JsonDict) {
override fun <T> sendObjectResponse(klass: Class<T>, response: T?, eventData: JsonDict) {
var jsString: String? = null
if (response != null) {
val objectAdapter = moshi.adapter(klass)
try {
jsString = "JSON.parse('${adapter.toJson(response)}')"
jsString = "JSON.parse('${objectAdapter.toJson(response)}')"
} catch (e: Exception) {
Timber.e(e, "## sendObjectResponse() : toJson failed ")
}
@ -130,7 +131,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(moshi: Moshi,
*/
override fun sendSuccess(eventData: JsonDict) {
val successResponse = mapOf("success" to true)
sendObjectResponse(successResponse, eventData)
sendObjectResponse(Map::class.java, successResponse, eventData)
}
/**
@ -147,7 +148,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(moshi: Moshi,
val subMap = HashMap<String, String>()
subMap["message"] = message
params["error"] = subMap
sendObjectResponse(params, eventData)
sendObjectResponse(Map::class.java, params, eventData)
}
/**

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.session.widgets
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Content
@ -42,6 +43,18 @@ internal class DefaultWidgetService @Inject constructor(private val widgetManage
return widgetManager.getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
}
override fun getRoomWidgetsLive(roomId: String, widgetId: QueryStringValue, widgetTypes: Set<String>?, excludedTypes: Set<String>?): LiveData<List<Widget>> {
return widgetManager.getRoomWidgetsLive(roomId, widgetId, widgetTypes, excludedTypes)
}
override fun getUserWidgetsLive(widgetTypes: Set<String>?, excludedTypes: Set<String>?): LiveData<List<Widget>> {
return widgetManager.getUserWidgetsLive(widgetTypes, excludedTypes)
}
override fun getUserWidgets(widgetTypes: Set<String>?, excludedTypes: Set<String>?): List<Widget> {
return widgetManager.getUserWidgets(widgetTypes, excludedTypes)
}
override fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback<Widget>): Cancelable {
return widgetManager.createWidget(roomId, widgetId, content, callback)
}

View File

@ -20,7 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.widgets.model.WidgetContent
data class Widget(
private val widgetContent: WidgetContent,
private val event: Event? = null
val widgetContent: WidgetContent,
val event: Event? = null
)

View File

@ -19,6 +19,8 @@ package im.vector.matrix.android.internal.session.widgets
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Content
@ -34,8 +36,9 @@ import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager
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.extractWidgets
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
import java.util.HashMap
@ -66,6 +69,19 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
}
fun getRoomWidgetsLive(
roomId: String,
widgetId: QueryStringValue = QueryStringValue.NoCondition,
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): LiveData<List<Widget>> {
// Get all im.vector.modular.widgets state events in the room
val liveWidgetEvents = stateEventDataSource.getStateEventsLive(roomId, setOf(WIDGET_EVENT_TYPE), widgetId)
return Transformations.map(liveWidgetEvents) { widgetEvents ->
widgetEvents.mapEventsToWidgets(widgetTypes, excludedTypes)
}
}
fun getRoomWidgets(
roomId: String,
widgetId: QueryStringValue = QueryStringValue.NoCondition,
@ -74,6 +90,12 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
): List<Widget> {
// Get all im.vector.modular.widgets state events in the room
val widgetEvents: List<Event> = stateEventDataSource.getStateEvents(roomId, setOf(WIDGET_EVENT_TYPE), widgetId)
return widgetEvents.mapEventsToWidgets(widgetTypes, excludedTypes)
}
private fun List<Event>.mapEventsToWidgets(widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null): List<Widget> {
val widgetEvents = this
// Widget id -> widget
val widgets: MutableMap<String, Widget> = HashMap()
// Order widgetEvents with the last event first
@ -102,12 +124,27 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
return widgets.values.toList()
}
fun getUserWidgetsLive(
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): LiveData<List<Widget>> {
val widgetsAccountData = accountDataDataSource.getLiveAccountDataEvent(UserAccountData.TYPE_WIDGETS)
return Transformations.map(widgetsAccountData) {
it.getOrNull()?.mapToWidgets(widgetTypes, excludedTypes) ?: emptyList()
}
}
fun getUserWidgets(
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): List<Widget> {
val widgetsAccountData = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_WIDGETS) ?: return emptyList()
return widgetsAccountData.extractWidgets()
return widgetsAccountData.mapToWidgets(widgetTypes, excludedTypes)
}
private fun UserAccountDataEvent.mapToWidgets(widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null): List<Widget> {
return extractWidgetSequence()
.filter {
val widgetType = it.type ?: return@filter false
(widgetTypes == null || widgetTypes.contains(widgetType))

View File

@ -20,7 +20,7 @@ import im.vector.matrix.android.R
import im.vector.matrix.android.api.session.widgets.WidgetURLBuilder
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManagerConfig
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig
import im.vector.matrix.android.internal.session.widgets.token.GetScalarTokenTask
import im.vector.matrix.android.internal.util.StringProvider
import java.net.URLEncoder

View File

@ -21,7 +21,7 @@ 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.internal.session.sync.model.accountdata.UserAccountDataEvent
internal fun UserAccountDataEvent.extractWidgets(): Sequence<WidgetContent> {
internal fun UserAccountDataEvent.extractWidgetSequence(): Sequence<WidgetContent> {
return content.asSequence()
.mapNotNull {
@Suppress("UNCHECKED_CAST")

View File

@ -99,7 +99,8 @@ import im.vector.riotx.features.signout.soft.SoftLogoutFragment
import im.vector.riotx.features.terms.ReviewTermsFragment
import im.vector.riotx.features.userdirectory.KnownUsersFragment
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
import im.vector.riotx.features.widgets.RoomWidgetFragment
import im.vector.riotx.features.widgets.admin.AdminWidgetFragment
import im.vector.riotx.features.widgets.room.RoomWidgetFragment
@Module
interface FragmentModule {
@ -479,11 +480,6 @@ interface FragmentModule {
@FragmentKey(SharedSecuredStorageKeyFragment::class)
fun bindSharedSecuredStorageKeyFragment(fragment: SharedSecuredStorageKeyFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomWidgetFragment::class)
fun bindWidgetFragment(fragment: RoomWidgetFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SetIdentityServerFragment::class)
@ -498,4 +494,14 @@ interface FragmentModule {
@IntoMap
@FragmentKey(ReviewTermsFragment::class)
fun bindReviewTermsFragment(fragment: ReviewTermsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomWidgetFragment::class)
fun bindRoomWidgetFragment(fragment: RoomWidgetFragment): Fragment
@Binds
@IntoMap
@FragmentKey(AdminWidgetFragment::class)
fun bindAdminWidgetFragment(fragment: AdminWidgetFragment): Fragment
}

View File

@ -215,6 +215,10 @@ class DefaultNavigator @Inject constructor(
fragment.startActivityForResult(intent, requestCode)
}
override fun openIntegrationManager(context: Context) {
//TODO
}
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
if (buildTask) {
val stackBuilder = TaskStackBuilder.create(context)

View File

@ -76,4 +76,7 @@ interface Navigator {
baseUrl: String,
token: String?,
requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE)
fun openIntegrationManager(context: Context, integId: String?, integType: String?)
}

View File

@ -0,0 +1,38 @@
/*
* 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
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator
import im.vector.matrix.android.api.util.JsonDict
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
class WidgetAPICallback(private val postAPIMediator: WidgetPostAPIMediator,
private val eventData: JsonDict,
private val stringProvider: StringProvider) : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
super.onFailure(failure)
postAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData)
}
override fun onSuccess(data: Unit) {
super.onSuccess(data)
postAPIMediator.sendSuccess(eventData)
}
}

View File

@ -0,0 +1,302 @@
/*
* 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
import android.content.Context
import android.text.TextUtils
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.Event
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.room.model.Membership
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.WidgetPostAPIMediator
import im.vector.matrix.android.api.util.JsonDict
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.navigation.Navigator
import timber.log.Timber
import java.util.HashMap
class WidgetPostAPIHandler(private val context: Context,
private val roomId: String,
private val navigator: Navigator,
private val stringProvider: StringProvider,
private val widgetPostAPIMediator: WidgetPostAPIMediator,
private val session: Session) : WidgetPostAPIMediator.Handler {
private val room = session.getRoom(roomId)!!
override fun handleWidgetRequest(eventData: JsonDict): Boolean {
return when (eventData["action"] as String?) {
"integration_manager_open" -> handleIntegrationManagerOpenAction(eventData).run { true }
"bot_options" -> getBotOptions(eventData).run { true }
"can_send_event" -> canSendEvent(eventData).run { true }
//"close_scalar" -> finish().run { true }
"get_membership_count" -> getMembershipCount(eventData).run { true }
//"get_widgets" -> getWidgets(eventData).run { true }
//"invite" -> inviteUser(eventData).run { true }
"join_rules_state" -> getJoinRules(eventData).run { true }
"membership_state" -> getMembershipState(eventData).run { true }
"set_bot_options" -> setBotOptions(eventData).run { true }
"set_bot_power" -> setBotPower(eventData).run { true }
"set_plumbing_state" -> setPlumbingState(eventData).run { true }
//"set_widget" -> setWidget(eventData).run { true }
else -> false
}
}
private fun handleIntegrationManagerOpenAction(eventData: JsonDict) {
var integType: String? = null
var integId: String? = null
val data = eventData["data"]
data
.takeIf { it is Map<*, *> }
?.let {
val dict = data as Map<*, *>
dict["integType"]
.takeIf { it is String }
?.let { integType = it as String }
dict["integId"]
.takeIf { it is String }
?.let { integId = it as String }
// Add "type_" as a prefix
integType?.let { integType = "type_$integType" }
}
navigator.openIntegrationManager(context, integId, integType)
}
/**
* Retrieve the latest botOptions event
*
* @param eventData the modular data
*/
private fun getBotOptions(eventData: JsonDict) {
if (checkRoomId(eventData) || checkUserId(eventData)) {
return
}
val userId = eventData["user_id"] as String
Timber.d("Received request to get options for bot $userId in room $roomId requested")
val stateEvents = room.getStateEvents(setOf(EventType.BOT_OPTIONS))
var botOptionsEvent: Event? = null
val stateKey = "_$userId"
for (stateEvent in stateEvents) {
if (TextUtils.equals(stateEvent.stateKey, stateKey)) {
if (null == botOptionsEvent || stateEvent.ageLocalTs ?: 0 > botOptionsEvent.ageLocalTs ?: 0) {
botOptionsEvent = stateEvent
}
}
}
if (null != botOptionsEvent) {
Timber.d("Received request to get options for bot $userId returns $botOptionsEvent")
widgetPostAPIMediator.sendObjectResponse(Event::class.java, botOptionsEvent, eventData)
} else {
Timber.d("Received request to get options for bot $userId returns null")
widgetPostAPIMediator.sendObjectResponse(Event::class.java, null, eventData)
}
}
private fun canSendEvent(eventData: JsonDict) {
if (checkRoomId(eventData)) {
return
}
Timber.d("Received request canSendEvent in room $roomId")
if (room.roomSummary()?.membership != Membership.JOIN) {
widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_must_be_in_room), eventData)
return
}
val eventType = eventData["event_type"] as String
val isState = eventData["is_state"] as Boolean
Timber.d("## canSendEvent() : eventType $eventType isState $isState")
val powerLevelsEvent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>()
val canSend = if (powerLevelsContent == null) {
false
} else {
PowerLevelsHelper(powerLevelsContent).isAllowedToSend(eventType, session.myUserId)
}
if (canSend) {
Timber.d("## canSendEvent() returns true")
widgetPostAPIMediator.sendBoolResponse(true, eventData)
} else {
Timber.d("## canSendEvent() returns widget_integration_no_permission_in_room")
widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_no_permission_in_room), eventData)
}
}
/**
* Provides the membership state
*
* @param eventData the modular data
*/
private fun getMembershipState(eventData: JsonDict) {
if (checkRoomId(eventData) || checkUserId(eventData)) {
return
}
val userId = eventData["user_id"] as String
Timber.d("membership_state of $userId in room $roomId requested")
val roomMemberStateEvent = room.getStateEvent(EventType.STATE_ROOM_MEMBER, stateKey = QueryStringValue.Equals(userId, QueryStringValue.Case.SENSITIVE))
}
/**
* Request the latest joined room event
*
* @param eventData the modular data
*/
private fun getJoinRules(eventData: JsonDict) {
if (checkRoomId(eventData)) {
return
}
Timber.d("Received request join rules in room $roomId")
val joinedEvents = room.getStateEvents(setOf(EventType.STATE_ROOM_JOIN_RULES))
if (joinedEvents.isNotEmpty()) {
widgetPostAPIMediator.sendObjectResponse(Event::class.java, joinedEvents.last(), eventData)
} else {
widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData)
}
}
/**
* Update the 'plumbing state"
*
* @param eventData the modular data
*/
private fun setPlumbingState(eventData: JsonDict) {
if (checkRoomId(eventData)) {
return
}
val description = "Received request to set plumbing state to status " + eventData["status"] + " in room " + roomId + " requested"
Timber.d(description)
val status = eventData["status"] as String
val params = HashMap<String, Any>()
params["status"] = status
room.sendStateEvent(
eventType = EventType.PLUMBING,
stateKey = null,
body = params,
callback = WidgetAPICallback(widgetPostAPIMediator, eventData, stringProvider)
)
}
/**
* Update the bot options
*
* @param eventData the modular data
*/
private fun setBotOptions(eventData: JsonDict) {
if (checkRoomId(eventData) || checkUserId(eventData)) {
return
}
val userId = eventData["user_id"] as String
val description = "Received request to set options for bot $userId in room $roomId"
Timber.d(description)
val content = eventData["content"] as JsonDict
val stateKey = "_$userId"
room.sendStateEvent(
eventType = EventType.BOT_OPTIONS,
stateKey = stateKey,
body = content,
callback = WidgetAPICallback(widgetPostAPIMediator, eventData, stringProvider))
}
/**
* Update the bot power levels
*
* @param eventData the modular data
*/
private fun setBotPower(eventData: JsonDict) {
if (checkRoomId(eventData) || checkUserId(eventData)) {
return
}
val userId = eventData["user_id"] as String
val description = "Received request to set power level to " + eventData["level"] + " for bot " + userId + " in room " + roomId
Timber.d(description)
val level = eventData["level"] as Int
if (level >= 0) {
// TODO
//room.updateUserPowerLevels(userId, level, WidgetApiCallback(eventData, description))
} else {
Timber.e("## setBotPower() : Power level must be positive integer.")
widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_positive_power_level), eventData)
}
}
/**
* Provides the number of members in the rooms
*
* @param eventData the modular data
*/
private fun getMembershipCount(eventData: JsonDict) {
if (checkRoomId(eventData)) {
return
}
val numberOfJoinedMembers = room.getNumberOfJoinedMembers()
widgetPostAPIMediator.sendIntegerResponse(numberOfJoinedMembers, eventData)
}
/**
* Check if roomId is present in the event and match
* Send response and return true in case of error
*
* @return true in case of error
*/
private fun checkRoomId(eventData: JsonDict): Boolean {
val roomIdInEvent = eventData["room_id"] as String?
// Check if param is present
if (null == roomIdInEvent) {
widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_missing_room_id), eventData)
return true
}
if (!TextUtils.equals(roomIdInEvent, roomId)) {
widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_room_not_visible), eventData)
return true
}
// OK
return false
}
/**
* Check if userId is present in the event
* Send response and return true in case of error
*
* @return true in case of error
*/
private fun checkUserId(eventData: JsonDict): Boolean {
val userIdInEvent = eventData["user_id"] as String?
// Check if param is present
if (null == userIdInEvent) {
widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_missing_user_id), eventData)
return true
}
// OK
return false
}
}

View File

@ -14,8 +14,8 @@
* limitations under the License.
*/
package im.vector.riotx.features.widgets
package im.vector.riotx.features.widgets.admin
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomWidgetAction : VectorViewModelAction
sealed class AdminWidgetAction : VectorViewModelAction

View File

@ -0,0 +1,49 @@
/*
* 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.admin
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import timber.log.Timber
import javax.inject.Inject
class AdminWidgetFragment @Inject constructor(
private val viewModelFactory: AdminWidgetViewModel.Factory
) : VectorBaseFragment(), AdminWidgetViewModel.Factory by viewModelFactory {
private val viewModel: AdminWidgetViewModel by fragmentViewModel()
override fun getLayoutResId() = R.layout.fragment_admin_widget
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Initialize your view, subscribe to viewModel...
}
override fun onDestroyView() {
super.onDestroyView()
// Clear your view, unsubscribe...
}
override fun invalidate() = withState(viewModel) { state ->
Timber.v("Invalidate with state: $state")
}
}

View File

@ -14,16 +14,8 @@
* limitations under the License.
*/
package im.vector.riotx.features.widgets
package im.vector.riotx.features.widgets.admin
import im.vector.matrix.android.api.util.JsonDict
import im.vector.riotx.core.platform.VectorViewEvents
// Example of data:
// {
// "event.data": {
// "action": "get_widgets",
// "room_id": "!byqyNXFYAGirEulaEm:matrix.org",
// "_id": "1526370173321-0.55myregve98-1"
// }
// }
typealias WidgetEventData = Map<String, JsonDict>
sealed class AdminWidgetViewEvents : VectorViewEvents

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.features.widgets
package im.vector.riotx.features.widgets.admin
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
@ -22,22 +22,20 @@ 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.session.Session
import im.vector.riotx.core.platform.VectorViewModel
class RoomWidgetViewModel @AssistedInject constructor(@Assisted initialState: WidgetViewState,
private val session: Session)
: VectorViewModel<WidgetViewState, RoomWidgetAction, RoomWidgetViewEvents>(initialState) {
class AdminWidgetViewModel @AssistedInject constructor(@Assisted initialState: AdminWidgetViewState)
: VectorViewModel<AdminWidgetViewState, AdminWidgetAction, AdminWidgetViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: WidgetViewState): RoomWidgetViewModel
fun create(initialState: AdminWidgetViewState): AdminWidgetViewModel
}
companion object : MvRxViewModelFactory<RoomWidgetViewModel, WidgetViewState> {
companion object : MvRxViewModelFactory<AdminWidgetViewModel, AdminWidgetViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: WidgetViewState): RoomWidgetViewModel? {
override fun create(viewModelContext: ViewModelContext, state: AdminWidgetViewState): AdminWidgetViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
@ -46,7 +44,7 @@ class RoomWidgetViewModel @AssistedInject constructor(@Assisted initialState: Wi
}
}
override fun handle(action: RoomWidgetAction) {
//TODO
override fun handle(action: AdminWidgetAction) {
}
}

View File

@ -0,0 +1,21 @@
/*
* 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.admin
import com.airbnb.mvrx.MvRxState
data class AdminWidgetViewState(val boolean: Boolean = false) : MvRxState

View File

@ -0,0 +1,25 @@
/*
* 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.room
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomWidgetAction : VectorViewModelAction {
data class OnWebViewStartedToLoad(val url: String) : RoomWidgetAction()
data class OnWebViewLoadingError(val url: String) : RoomWidgetAction()
data class OnWebViewLoadingSuccess(val url: String) : RoomWidgetAction()
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.features.widgets
package im.vector.riotx.features.widgets.room
import android.os.Bundle
import android.os.Parcelable
@ -22,6 +22,8 @@ import android.view.View
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.widgets.WidgetPostAPIMediator
import im.vector.matrix.android.api.util.JsonDict
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.webview.WebViewEventListener
@ -34,12 +36,15 @@ import javax.inject.Inject
@Parcelize
data class WidgetArgs(
val widgetId: String
val baseUrl: String,
val kind: WidgetKind,
val roomId: String,
val widgetId: String? = null
) : Parcelable
class RoomWidgetFragment @Inject constructor(
private val viewModelFactory: RoomWidgetViewModel.Factory
) : VectorBaseFragment(), RoomWidgetViewModel.Factory by viewModelFactory, WebViewEventListener {
) : VectorBaseFragment(), RoomWidgetViewModel.Factory by viewModelFactory, WebViewEventListener, WidgetPostAPIMediator.Handler {
private val fragmentArgs: WidgetArgs by args()
private val viewModel: RoomWidgetViewModel by fragmentViewModel()
@ -49,10 +54,12 @@ class RoomWidgetFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
widgetWebView.setupForWidget(this)
viewModel.getPostAPIMediator().initialize(widgetWebView, this)
}
override fun onDestroyView() {
super.onDestroyView()
viewModel.getPostAPIMediator().clear()
widgetWebView.clearAfterWidget()
}
@ -77,6 +84,7 @@ class RoomWidgetFragment @Inject constructor(
}
override fun onPageStarted(url: String) {
}
override fun onPageFinished(url: String) {
@ -85,4 +93,8 @@ class RoomWidgetFragment @Inject constructor(
override fun onPageError(url: String, errorCode: Int, description: String) {
}
override fun handleWidgetRequest(eventData: JsonDict): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.features.widgets
package im.vector.riotx.features.widgets.room
import im.vector.riotx.core.platform.VectorViewEvents

View File

@ -0,0 +1,129 @@
/*
* 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.room
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
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.riotx.core.platform.VectorViewModel
import kotlinx.coroutines.launch
class RoomWidgetViewModel @AssistedInject constructor(@Assisted val initialState: WidgetViewState,
private val session: Session)
: VectorViewModel<WidgetViewState, RoomWidgetAction, RoomWidgetViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: WidgetViewState): RoomWidgetViewModel
}
companion object : MvRxViewModelFactory<RoomWidgetViewModel, WidgetViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: WidgetViewState): RoomWidgetViewModel? {
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")
}
}
private val widgetService = session.widgetService()
private val integrationManagerService = session.integrationManagerService()
private val widgetBuilder = widgetService.getWidgetURLBuilder()
private val postAPIMediator = widgetService.getWidgetPostAPIMediator()
init {
refreshPermissionStatus()
}
fun getPostAPIMediator() = postAPIMediator
override fun handle(action: RoomWidgetAction) {
when (action) {
is RoomWidgetAction.OnWebViewLoadingError -> handleWebViewLoadingError(action.url)
is RoomWidgetAction.OnWebViewLoadingSuccess -> handleWebViewLoadingSuccess(action.url)
is RoomWidgetAction.OnWebViewStartedToLoad -> handleWebViewStartLoading(action.url)
}
}
private fun refreshPermissionStatus() {
if (initialState.widgetKind == WidgetKind.USER || initialState.widgetKind == WidgetKind.INTEGRATION_MANAGER) {
onWidgetAllowed()
} else {
val widgetId = initialState.widgetId
if (widgetId == null) {
setState { copy(status = WidgetStatus.WIDGET_NOT_ALLOWED) }
return
}
val roomWidget = widgetService.getRoomWidgets(initialState.roomId, widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.SENSITIVE)).firstOrNull()
if (roomWidget == null) {
setState { copy(status = WidgetStatus.WIDGET_NOT_ALLOWED) }
return
}
if (roomWidget.event?.senderId == session.myUserId) {
onWidgetAllowed()
} else {
val stateEventId = roomWidget.event?.eventId
// This should not happen
if (stateEventId == null) {
setState { copy(status = WidgetStatus.WIDGET_NOT_ALLOWED) }
return
}
val isAllowed = integrationManagerService.isWidgetAllowed(stateEventId)
if (!isAllowed) {
setState { copy(status = WidgetStatus.WIDGET_NOT_ALLOWED) }
} else {
onWidgetAllowed()
}
}
}
}
private fun onWidgetAllowed() {
setState {
copy(status = WidgetStatus.WIDGET_ALLOWED, formattedURL = Loading())
}
viewModelScope.launch {
try {
val formattedUrl = widgetBuilder.build(initialState.baseUrl)
setState { copy(formattedURL = Success(formattedUrl)) }
} catch (failure: Throwable) {
setState { copy(formattedURL = Fail(failure)) }
}
}
}
private fun handleWebViewStartLoading(url: String) {
}
private fun handleWebViewLoadingSuccess(url: String) {
}
private fun handleWebViewLoadingError(url: String) {
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.features.widgets
package im.vector.riotx.features.widgets.room
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
@ -26,8 +26,17 @@ enum class WidgetStatus {
WIDGET_ALLOWED
}
enum class WidgetKind {
ROOM,
USER,
INTEGRATION_MANAGER
}
data class WidgetViewState(
val widgetId: String,
val roomId: String,
val baseUrl: String,
val widgetId: String? = null,
val widgetKind: WidgetKind,
val status: WidgetStatus = WidgetStatus.UNKNOWN,
val formattedURL: Async<String> = Uninitialized,
val webviewLoadedUrl: Async<String> = Uninitialized,
@ -36,5 +45,10 @@ data class WidgetViewState(
val createdByMe: Boolean = false
) : MvRxState {
constructor(widgetArgs: WidgetArgs) : this(widgetId = widgetArgs.widgetId)
constructor(widgetArgs: WidgetArgs) : this(
widgetKind = widgetArgs.kind,
baseUrl = widgetArgs.baseUrl,
roomId = widgetArgs.roomId,
widgetId = widgetArgs.widgetId
)
}

View File

@ -0,0 +1,42 @@
/*
* 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.room
interface WidgetParams {
val params: Map<String, String>
}
class IntegrationManagerParams(
private val widgetId: String? = null,
private val screenId: String? = null) : WidgetParams {
override val params: Map<String, String> by lazy {
buildParams()
}
private fun buildParams(): Map<String, String> {
val map = HashMap<String, String>()
if (widgetId != null) {
map["integ_id"] = widgetId
}
if (screenId != null) {
map["screen"] = screenId
}
return map
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rootConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AdminWidgetFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>