Merge branch 'develop' into feature/fix_1543

This commit is contained in:
Benoit Marty 2020-06-24 21:24:25 +02:00 committed by GitHub
commit c0c300925d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 229 additions and 179 deletions

View file

@ -12,6 +12,7 @@ Bugfix 🐛:
- Fix dark theme issue on login screen (#1097) - Fix dark theme issue on login screen (#1097)
- Incomplete predicate in RealmCryptoStore#getOutgoingRoomKeyRequest (#1519) - Incomplete predicate in RealmCryptoStore#getOutgoingRoomKeyRequest (#1519)
- User could not redact message that they have sent (#1543) - User could not redact message that they have sent (#1543)
- Use vendor prefix for non merged MSC (#1537)
Translations 🗣: Translations 🗣:
- -
@ -27,7 +28,7 @@ Other changes:
- Use `retrofit2.Call.awaitResponse` extension provided by Retrofit 2. (#1526) - Use `retrofit2.Call.awaitResponse` extension provided by Retrofit 2. (#1526)
- Fix minor typo in contribution guide (#1512) - Fix minor typo in contribution guide (#1512)
- Fix self-assignment of callback in `DefaultRoomPushRuleService#setRoomNotificationState` (#1520) - Fix self-assignment of callback in `DefaultRoomPushRuleService#setRoomNotificationState` (#1520)
- Random housekeeping clean-ups indicated by Lint (#1520) - Random housekeeping clean-ups indicated by Lint (#1520, #1541)
Changes in RiotX 0.22.0 (2020-06-15) Changes in RiotX 0.22.0 (2020-06-15)
=================================================== ===================================================

View file

@ -241,14 +241,14 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
val eventWireContent = event.content.toContent() val eventWireContent = event.content.toContent()
assertNotNull(eventWireContent) assertNotNull(eventWireContent)
assertNull(eventWireContent.get("body")) assertNull(eventWireContent["body"])
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent.get("algorithm")) assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"])
assertNotNull(eventWireContent.get("ciphertext")) assertNotNull(eventWireContent["ciphertext"])
assertNotNull(eventWireContent.get("session_id")) assertNotNull(eventWireContent["session_id"])
assertNotNull(eventWireContent.get("sender_key")) assertNotNull(eventWireContent["sender_key"])
assertEquals(senderSession.sessionParams.deviceId, eventWireContent.get("device_id")) assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"])
assertNotNull(event.eventId) assertNotNull(event.eventId)
assertEquals(roomId, event.roomId) assertEquals(roomId, event.roomId)
@ -257,7 +257,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
val eventContent = event.toContent() val eventContent = event.toContent()
assertNotNull(eventContent) assertNotNull(eventContent)
assertEquals(clearMessage, eventContent.get("body")) assertEquals(clearMessage, eventContent["body"])
assertEquals(senderSession.myUserId, event.senderId) assertEquals(senderSession.myUserId, event.senderId)
} }

View file

@ -144,7 +144,7 @@ class QuadSTests : InstrumentedTest {
val secretAccountData = assertAccountData(aliceSession, "secret.of.life") val secretAccountData = assertAccountData(aliceSession, "secret.of.life")
val encryptedContent = secretAccountData.content.get("encrypted") as? Map<*, *> val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *>
assertNotNull("Element should be encrypted", encryptedContent) assertNotNull("Element should be encrypted", encryptedContent)
assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId)) assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId))

View file

@ -87,14 +87,13 @@ class EventMatchCondition(
// Very simple glob to regexp converter // Very simple glob to regexp converter
private fun simpleGlobToRegExp(glob: String): String { private fun simpleGlobToRegExp(glob: String): String {
var out = "" // "^" var out = "" // "^"
for (i in 0 until glob.length) { for (element in glob) {
val c = glob[i] when (element) {
when (c) {
'*' -> out += ".*" '*' -> out += ".*"
'?' -> out += '.'.toString() '?' -> out += '.'.toString()
'.' -> out += "\\." '.' -> out += "\\."
'\\' -> out += "\\\\" '\\' -> out += "\\\\"
else -> out += c else -> out += element
} }
} }
out += "" // '$'.toString() out += "" // '$'.toString()

View file

@ -26,5 +26,5 @@ object RelationType {
/** Lets you define an event which references an existing event.*/ /** Lets you define an event which references an existing event.*/
const val REFERENCE = "m.reference" const val REFERENCE = "m.reference"
/** Lets you define an event which adds a response to an existing event.*/ /** Lets you define an event which adds a response to an existing event.*/
const val RESPONSE = "m.response" const val RESPONSE = "org.matrix.response"
} }

View file

@ -90,6 +90,6 @@ interface WidgetPostAPIMediator {
/** /**
* Triggered when a widget is posting * Triggered when a widget is posting
*/ */
fun handleWidgetRequest(eventData: JsonDict): Boolean fun handleWidgetRequest(mediator: WidgetPostAPIMediator, eventData: JsonDict): Boolean
} }
} }

View file

@ -34,7 +34,8 @@ interface WidgetService {
fun getWidgetURLFormatter(): WidgetURLFormatter fun getWidgetURLFormatter(): WidgetURLFormatter
/** /**
* Returns an instance of [WidgetPostAPIMediator]. * Returns a new instance of [WidgetPostAPIMediator].
* Be careful to call clearWebView method and setHandler to null to avoid memory leaks.
* This is to be used for "admin" widgets so you can interact through JS. * This is to be used for "admin" widgets so you can interact through JS.
*/ */
fun getWidgetPostAPIMediator(): WidgetPostAPIMediator fun getWidgetPostAPIMediator(): WidgetPostAPIMediator

View file

@ -16,6 +16,22 @@
package im.vector.matrix.android.api.session.widgets.model package im.vector.matrix.android.api.session.widgets.model
private val DEFINED_TYPES by lazy {
listOf(
WidgetType.Jitsi,
WidgetType.TradingView,
WidgetType.Spotify,
WidgetType.Video,
WidgetType.GoogleDoc,
WidgetType.GoogleCalendar,
WidgetType.Etherpad,
WidgetType.StickerPicker,
WidgetType.Grafana,
WidgetType.Custom,
WidgetType.IntegrationManager
)
}
/** /**
* Ref: https://github.com/matrix-org/matrix-doc/issues/1236 * Ref: https://github.com/matrix-org/matrix-doc/issues/1236
*/ */
@ -33,7 +49,7 @@ sealed class WidgetType(open val preferred: String, open val legacy: String = pr
object IntegrationManager : WidgetType("m.integration_manager") object IntegrationManager : WidgetType("m.integration_manager")
data class Fallback(override val preferred: String) : WidgetType(preferred) data class Fallback(override val preferred: String) : WidgetType(preferred)
fun matches(type: String?): Boolean { fun matches(type: String): Boolean {
return type == preferred || type == legacy return type == preferred || type == legacy
} }
@ -43,20 +59,6 @@ sealed class WidgetType(open val preferred: String, open val legacy: String = pr
companion object { companion object {
private val DEFINED_TYPES = listOf(
Jitsi,
TradingView,
Spotify,
Video,
GoogleDoc,
GoogleCalendar,
Etherpad,
StickerPicker,
Grafana,
Custom,
IntegrationManager
)
fun fromString(type: String): WidgetType { fun fromString(type: String): WidgetType {
val matchingType = DEFINED_TYPES.firstOrNull { val matchingType = DEFINED_TYPES.firstOrNull {
it.matches(type) it.matches(type)

View file

@ -273,7 +273,7 @@ internal abstract class SASDefaultVerificationTransaction(
if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) { if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) {
// Check the signature // Check the signature
val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it) val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it)
if (mac != theirMacSafe.mac.get(it)) { if (mac != theirMacSafe.mac[it]) {
// WRONG! // WRONG!
Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix") Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)

View file

@ -25,7 +25,7 @@ internal object TimelineEventFilter {
*/ */
internal object Content { internal object Content {
internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"m.response"*}""" internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}"""
} }
/** /**

View file

@ -78,7 +78,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh
private fun onWidgetMessage(eventData: JsonDict) { private fun onWidgetMessage(eventData: JsonDict) {
try { try {
if (handler?.handleWidgetRequest(eventData) == false) { if (handler?.handleWidgetRequest(this, eventData) == false) {
sendError("", eventData) sendError("", eventData)
} }
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -26,10 +26,11 @@ import im.vector.matrix.android.api.session.widgets.WidgetURLFormatter
import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
internal class DefaultWidgetService @Inject constructor(private val widgetManager: WidgetManager, internal class DefaultWidgetService @Inject constructor(private val widgetManager: WidgetManager,
private val widgetURLFormatter: WidgetURLFormatter, private val widgetURLFormatter: WidgetURLFormatter,
private val widgetPostAPIMediator: WidgetPostAPIMediator) private val widgetPostAPIMediator: Provider<WidgetPostAPIMediator>)
: WidgetService { : WidgetService {
override fun getWidgetURLFormatter(): WidgetURLFormatter { override fun getWidgetURLFormatter(): WidgetURLFormatter {
@ -37,7 +38,7 @@ internal class DefaultWidgetService @Inject constructor(private val widgetManage
} }
override fun getWidgetPostAPIMediator(): WidgetPostAPIMediator { override fun getWidgetPostAPIMediator(): WidgetPostAPIMediator {
return widgetPostAPIMediator return widgetPostAPIMediator.get()
} }
override fun getRoomWidgets( override fun getRoomWidgets(

View file

@ -67,8 +67,7 @@ internal class DefaultGetScalarTokenTask @Inject constructor(private val widgets
throw IllegalStateException("Scalar token is null") throw IllegalStateException("Scalar token is null")
} }
scalarTokenStore.setToken(serverUrl, registerWidgetResponse.scalarToken) scalarTokenStore.setToken(serverUrl, registerWidgetResponse.scalarToken)
widgetsAPI.validateToken(registerWidgetResponse.scalarToken, WIDGET_API_VERSION) return validateToken(widgetsAPI, serverUrl, registerWidgetResponse.scalarToken)
return registerWidgetResponse.scalarToken
} }
private suspend fun validateToken(widgetsAPI: WidgetsAPI, serverUrl: String, scalarToken: String): String { private suspend fun validateToken(widgetsAPI: WidgetsAPI, serverUrl: String, scalarToken: String): String {

View file

@ -62,9 +62,9 @@ class ContactPicker(override val requestCode: Int) : Picker<MultiPickerContactTy
val contactId = cursor.getInt(idColumn) val contactId = cursor.getInt(idColumn)
var name = cursor.getString(nameColumn) var name = cursor.getString(nameColumn)
var photoUri = cursor.getString(photoUriColumn) val photoUri = cursor.getString(photoUriColumn)
var phoneNumberList = mutableListOf<String>() val phoneNumberList = mutableListOf<String>()
var emailList = mutableListOf<String>() val emailList = mutableListOf<String>()
getRawContactId(context.contentResolver, contactId)?.let { rawContactId -> getRawContactId(context.contentResolver, contactId)?.let { rawContactId ->
val selection = ContactsContract.Data.RAW_CONTACT_ID + " = ?" val selection = ContactsContract.Data.RAW_CONTACT_ID + " = ?"

View file

@ -40,14 +40,14 @@ class DefaultErrorFormatter @Inject constructor(
null -> null null -> null
is IdentityServiceError -> identityServerError(throwable) is IdentityServiceError -> identityServerError(throwable)
is Failure.NetworkConnection -> { is Failure.NetworkConnection -> {
when { when (throwable.ioException) {
throwable.ioException is SocketTimeoutException -> is SocketTimeoutException ->
stringProvider.getString(R.string.error_network_timeout) stringProvider.getString(R.string.error_network_timeout)
throwable.ioException is UnknownHostException -> is UnknownHostException ->
// Invalid homeserver? // Invalid homeserver?
// TODO Check network state, airplane mode, etc. // TODO Check network state, airplane mode, etc.
stringProvider.getString(R.string.login_error_unknown_host) stringProvider.getString(R.string.login_error_unknown_host)
else -> else ->
stringProvider.getString(R.string.error_no_network) stringProvider.getString(R.string.error_no_network)
} }
} }

View file

@ -45,7 +45,7 @@ class VectorEditTextPreference : EditTextPreference {
override fun onBindViewHolder(holder: PreferenceViewHolder) { override fun onBindViewHolder(holder: PreferenceViewHolder) {
// display the title in multi-line to avoid ellipsis. // display the title in multi-line to avoid ellipsis.
try { try {
holder.itemView.findViewById<TextView>(android.R.id.title)?.setSingleLine(false) holder.itemView.findViewById<TextView>(android.R.id.title)?.isSingleLine = false
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "onBindView") Timber.e(e, "onBindView")
} }

View file

@ -87,7 +87,7 @@ open class VectorPreference : Preference {
val title = itemView.findViewById<TextView>(android.R.id.title) val title = itemView.findViewById<TextView>(android.R.id.title)
val summary = itemView.findViewById<TextView>(android.R.id.summary) val summary = itemView.findViewById<TextView>(android.R.id.summary)
if (title != null) { if (title != null) {
title.setSingleLine(false) title.isSingleLine = false
title.setTypeface(null, mTypeface) title.setTypeface(null, mTypeface)
} }

View file

@ -43,7 +43,7 @@ class VectorSwitchPreference : SwitchPreference {
override fun onBindViewHolder(holder: PreferenceViewHolder) { override fun onBindViewHolder(holder: PreferenceViewHolder) {
// display the title in multi-line to avoid ellipsis. // display the title in multi-line to avoid ellipsis.
holder.itemView.findViewById<TextView>(android.R.id.title)?.setSingleLine(false) holder.itemView.findViewById<TextView>(android.R.id.title)?.isSingleLine = false
super.onBindViewHolder(holder) super.onBindViewHolder(holder)
} }

View file

@ -78,4 +78,5 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class ReRequestKeys(val eventId: String) : RoomDetailAction() data class ReRequestKeys(val eventId: String) : RoomDetailAction()
object SelectStickerAttachment : RoomDetailAction() object SelectStickerAttachment : RoomDetailAction()
object OpenIntegrationManager: RoomDetailAction()
} }

View file

@ -144,7 +144,6 @@ import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.composer.TextComposerView import im.vector.riotx.features.home.room.detail.composer.TextComposerView
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.sticker.StickerPickerConstants
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.action.EventSharedAction import im.vector.riotx.features.home.room.detail.timeline.action.EventSharedAction
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
@ -159,6 +158,7 @@ import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.riotx.features.home.room.detail.widget.RoomWidgetsBannerView import im.vector.riotx.features.home.room.detail.widget.RoomWidgetsBannerView
import im.vector.riotx.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.riotx.features.home.room.detail.widget.RoomWidgetsBottomSheet
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.invite.VectorInviteView
@ -330,22 +330,33 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.observeViewEvents { roomDetailViewModel.observeViewEvents {
when (it) { when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it) is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG) is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG)
is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it) is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it) is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it)
is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it)
is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it)
RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager() RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager()
is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it) is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it)
is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog()
is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager()
}.exhaustive }.exhaustive
} }
} }
private fun openIntegrationManager(screen: String? = null) {
navigator.openIntegrationManager(
fragment = this,
roomId = roomDetailArgs.roomId,
integId = null,
screen = screen
)
}
private fun setupWidgetsBannerView() { private fun setupWidgetsBannerView() {
roomWidgetsBannerView.callback = this roomWidgetsBannerView.callback = this
} }
@ -362,10 +373,7 @@ class RoomDetailFragment @Inject constructor(
.setView(v) .setView(v)
.setPositiveButton(R.string.yes) { _, _ -> .setPositiveButton(R.string.yes) { _, _ ->
// Open integration manager, to the sticker installation page // Open integration manager, to the sticker installation page
navigator.openIntegrationManager( openIntegrationManager(
context = requireContext(),
roomId = roomDetailArgs.roomId,
integId = null,
screen = WidgetType.StickerPicker.preferred screen = WidgetType.StickerPicker.preferred
) )
} }
@ -508,11 +516,7 @@ class RoomDetailFragment @Inject constructor(
true true
} }
R.id.open_matrix_apps -> { R.id.open_matrix_apps -> {
if (session.integrationManagerService().isIntegrationEnabled()) { roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager)
navigator.openIntegrationManager(requireContext(), roomDetailArgs.roomId, null, null)
} else {
displayDisabledIntegrationDialog()
}
true true
} }
R.id.voice_call, R.id.voice_call,
@ -645,16 +649,16 @@ class RoomDetailFragment @Inject constructor(
val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data) val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
if (!hasBeenHandled && resultCode == RESULT_OK && data != null) { if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
when (requestCode) { when (requestCode) {
AttachmentsPreviewActivity.REQUEST_CODE -> { AttachmentsPreviewActivity.REQUEST_CODE -> {
val sendData = AttachmentsPreviewActivity.getOutput(data) val sendData = AttachmentsPreviewActivity.getOutput(data)
val keepOriginalSize = AttachmentsPreviewActivity.getKeepOriginalSize(data) val keepOriginalSize = AttachmentsPreviewActivity.getKeepOriginalSize(data)
roomDetailViewModel.handle(RoomDetailAction.SendMedia(sendData, !keepOriginalSize)) roomDetailViewModel.handle(RoomDetailAction.SendMedia(sendData, !keepOriginalSize))
} }
REACTION_SELECT_REQUEST_CODE -> { REACTION_SELECT_REQUEST_CODE -> {
val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return
roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction)) roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction))
} }
StickerPickerConstants.STICKER_PICKER_REQUEST_CODE -> { WidgetRequestCodes.STICKER_PICKER_REQUEST_CODE -> {
val content = WidgetActivity.getOutput(data).toModel<MessageStickerContent>() ?: return val content = WidgetActivity.getOutput(data).toModel<MessageStickerContent>() ?: return
roomDetailViewModel.handle(RoomDetailAction.SendSticker(content)) roomDetailViewModel.handle(RoomDetailAction.SendSticker(content))
} }

View file

@ -52,8 +52,12 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
object DisplayPromptForIntegrationManager: RoomDetailViewEvents() object DisplayPromptForIntegrationManager: RoomDetailViewEvents()
object DisplayEnableIntegrationsWarning: RoomDetailViewEvents()
data class OpenStickerPicker(val widget: Widget): RoomDetailViewEvents() data class OpenStickerPicker(val widget: Widget): RoomDetailViewEvents()
object OpenIntegrationManager: RoomDetailViewEvents()
object MessageSent : SendMessageResult() object MessageSent : SendMessageResult()
data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult()
class SlashCommandError(val command: Command) : SendMessageResult() class SlashCommandError(val command: Command) : SendMessageResult()

View file

@ -81,7 +81,9 @@ import io.reactivex.Observable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber import timber.log.Timber
@ -257,6 +259,7 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.EndCall -> handleEndCall() is RoomDetailAction.EndCall -> handleEndCall()
}.exhaustive }.exhaustive
@ -283,6 +286,19 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
private fun handleOpenIntegrationManager() {
viewModelScope.launch {
val viewEvent = withContext(Dispatchers.Default) {
if (isIntegrationEnabled()) {
RoomDetailViewEvents.OpenIntegrationManager
} else {
RoomDetailViewEvents.DisplayEnableIntegrationsWarning
}
}
_viewEvents.post(viewEvent)
}
}
private fun startTrackingUnreadMessages() { private fun startTrackingUnreadMessages() {
trackUnreadMessages.set(true) trackUnreadMessages.set(true)
setState { copy(canShowJumpToReadMarker = false) } setState { copy(canShowJumpToReadMarker = false) }
@ -382,6 +398,8 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled()
fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) { fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) {
R.id.clear_message_queue -> R.id.clear_message_queue ->
// For now always disable when not in developer mode, worker cancellation is not working properly // For now always disable when not in developer mode, worker cancellation is not working properly

View file

@ -33,6 +33,7 @@ import com.airbnb.epoxy.EpoxyTouchHelperCallback
import com.airbnb.epoxy.EpoxyViewHolder import com.airbnb.epoxy.EpoxyViewHolder
import timber.log.Timber import timber.log.Timber
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min
class RoomMessageTouchHelperCallback(private val context: Context, class RoomMessageTouchHelperCallback(private val context: Context,
@DrawableRes actionIcon: Int, @DrawableRes actionIcon: Int,
@ -92,7 +93,7 @@ class RoomMessageTouchHelperCallback(private val context: Context,
setTouchListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) setTouchListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
} }
val size = triggerDistance val size = triggerDistance
if (Math.abs(viewHolder.itemView.translationX) < size || dX > this.dX /*going back*/) { if (abs(viewHolder.itemView.translationX) < size || dX > this.dX /*going back*/) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
this.dX = dX this.dX = dX
startTracking = true startTracking = true
@ -127,9 +128,9 @@ class RoomMessageTouchHelperCallback(private val context: Context,
private fun drawReplyButton(canvas: Canvas, itemView: View) { private fun drawReplyButton(canvas: Canvas, itemView: View) {
// Timber.v("drawReplyButton") // Timber.v("drawReplyButton")
val translationX = Math.abs(itemView.translationX) val translationX = abs(itemView.translationX)
val newTime = System.currentTimeMillis() val newTime = System.currentTimeMillis()
val dt = Math.min(17, newTime - lastReplyButtonAnimationTime) val dt = min(17, newTime - lastReplyButtonAnimationTime)
lastReplyButtonAnimationTime = newTime lastReplyButtonAnimationTime = newTime
val showing = translationX >= minShowDistance val showing = translationX >= minShowDistance
if (showing) { if (showing) {
@ -163,10 +164,10 @@ class RoomMessageTouchHelperCallback(private val context: Context,
} else { } else {
1.2f - 0.2f * ((replyButtonProgress - 0.8f) / 0.2f) 1.2f - 0.2f * ((replyButtonProgress - 0.8f) / 0.2f)
} }
alpha = Math.min(255f, 255 * (replyButtonProgress / 0.8f)).toInt() alpha = min(255f, 255 * (replyButtonProgress / 0.8f)).toInt()
} else { } else {
scale = replyButtonProgress scale = replyButtonProgress
alpha = Math.min(255f, 255 * replyButtonProgress).toInt() alpha = min(255f, 255 * replyButtonProgress).toInt()
} }
imageDrawable.alpha = alpha imageDrawable.alpha = alpha

View file

@ -27,6 +27,10 @@ class StickerPickerActionHandler @Inject constructor(private val session: Sessio
suspend fun handle(): RoomDetailViewEvents = withContext(Dispatchers.Default) { suspend fun handle(): RoomDetailViewEvents = withContext(Dispatchers.Default) {
// Search for the sticker picker widget in the user account // Search for the sticker picker widget in the user account
val integrationsEnabled = session.integrationManagerService().isIntegrationEnabled()
if (!integrationsEnabled) {
return@withContext RoomDetailViewEvents.DisplayEnableIntegrationsWarning
}
val stickerWidget = session.widgetService().getUserWidgets(WidgetType.StickerPicker.values()).firstOrNull { it.isActive } val stickerWidget = session.widgetService().getUserWidgets(WidgetType.StickerPicker.values()).firstOrNull { it.isActive }
if (stickerWidget == null || stickerWidget.computedUrl.isNullOrBlank()) { if (stickerWidget == null || stickerWidget.computedUrl.isNullOrBlank()) {
RoomDetailViewEvents.DisplayPromptForIntegrationManager RoomDetailViewEvents.DisplayPromptForIntegrationManager

View file

@ -64,8 +64,8 @@ class PollResultLineView @JvmOverloads constructor(
set(value) { set(value) {
field = value field = value
// Text in main color // Text in main color
labelTextView.setTypeface(labelTextView.getTypeface(), if (value) Typeface.BOLD else Typeface.NORMAL) labelTextView.setTypeface(labelTextView.typeface, if (value) Typeface.BOLD else Typeface.NORMAL)
percentTextView.setTypeface(percentTextView.getTypeface(), if (value) Typeface.BOLD else Typeface.NORMAL) percentTextView.setTypeface(percentTextView.typeface, if (value) Typeface.BOLD else Typeface.NORMAL)
} }
init { init {

View file

@ -14,8 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.home.room.detail.sticker package im.vector.riotx.features.home.room.detail.widget
object StickerPickerConstants { object WidgetRequestCodes {
const val STICKER_PICKER_REQUEST_CODE = 16000 const val STICKER_PICKER_REQUEST_CODE = 16000
const val INTEGRATION_MANAGER_REQUEST_CODE = 16001
} }

View file

@ -30,8 +30,8 @@ import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.error.fatalError import im.vector.riotx.core.error.fatalError
@ -46,7 +46,7 @@ import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import im.vector.riotx.features.debug.DebugMenuActivity import im.vector.riotx.features.debug.DebugMenuActivity
import im.vector.riotx.features.home.room.detail.RoomDetailActivity import im.vector.riotx.features.home.room.detail.RoomDetailActivity
import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.detail.sticker.StickerPickerConstants import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity import im.vector.riotx.features.invite.InviteUsersToRoomActivity
import im.vector.riotx.features.media.BigImageViewerActivity import im.vector.riotx.features.media.BigImageViewerActivity
@ -230,12 +230,13 @@ class DefaultNavigator @Inject constructor(
override fun openStickerPicker(fragment: Fragment, roomId: String, widget: Widget, requestCode: Int) { override fun openStickerPicker(fragment: Fragment, roomId: String, widget: Widget, requestCode: Int) {
val widgetArgs = widgetArgsBuilder.buildStickerPickerArgs(roomId, widget) val widgetArgs = widgetArgsBuilder.buildStickerPickerArgs(roomId, widget)
val intent = WidgetActivity.newIntent(fragment.requireContext(), widgetArgs) val intent = WidgetActivity.newIntent(fragment.requireContext(), widgetArgs)
fragment.startActivityForResult(intent, StickerPickerConstants.STICKER_PICKER_REQUEST_CODE) fragment.startActivityForResult(intent, WidgetRequestCodes.STICKER_PICKER_REQUEST_CODE)
} }
override fun openIntegrationManager(context: Context, roomId: String, integId: String?, screen: String?) { override fun openIntegrationManager(fragment: Fragment, roomId: String, integId: String?, screen: String?) {
val widgetArgs = widgetArgsBuilder.buildIntegrationManagerArgs(roomId, integId, screen) val widgetArgs = widgetArgsBuilder.buildIntegrationManagerArgs(roomId, integId, screen)
context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) val intent = WidgetActivity.newIntent(fragment.requireContext(), widgetArgs)
fragment.startActivityForResult(intent, WidgetRequestCodes.INTEGRATION_MANAGER_REQUEST_CODE)
} }
override fun openRoomWidget(context: Context, roomId: String, widget: Widget) { override fun openRoomWidget(context: Context, roomId: String, widget: Widget) {

View file

@ -25,7 +25,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.riotx.features.home.room.detail.sticker.StickerPickerConstants import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.settings.VectorSettingsActivity
@ -85,9 +85,9 @@ interface Navigator {
fun openStickerPicker(fragment: Fragment, fun openStickerPicker(fragment: Fragment,
roomId: String, roomId: String,
widget: Widget, widget: Widget,
requestCode: Int = StickerPickerConstants.STICKER_PICKER_REQUEST_CODE) requestCode: Int = WidgetRequestCodes.STICKER_PICKER_REQUEST_CODE)
fun openIntegrationManager(context: Context, roomId: String, integId: String?, screen: String?) fun openIntegrationManager(fragment: Fragment, roomId: String, integId: String?, screen: String?)
fun openRoomWidget(context: Context, roomId: String, widget: Widget) fun openRoomWidget(context: Context, roomId: String, widget: Widget)

View file

@ -22,6 +22,8 @@ import android.graphics.Paint
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Property import android.util.Property
import android.view.View import android.view.View
import kotlin.math.cos
import kotlin.math.sin
/** /**
* This view will draw dots floating around the center of it's view * This view will draw dots floating around the center of it's view
@ -84,16 +86,16 @@ class DotsView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
private fun drawOuterDotsFrame(canvas: Canvas) { private fun drawOuterDotsFrame(canvas: Canvas) {
for (i in 0 until DOTS_COUNT) { for (i in 0 until DOTS_COUNT) {
val cX = (centerX + currentRadius1 * Math.cos(i.toDouble() * OUTER_DOTS_POSITION_ANGLE.toDouble() * Math.PI / 180)).toFloat() val cX = (centerX + currentRadius1 * cos(i.toDouble() * OUTER_DOTS_POSITION_ANGLE.toDouble() * Math.PI / 180)).toFloat()
val cY = (centerY + currentRadius1 * Math.sin(i.toDouble() * OUTER_DOTS_POSITION_ANGLE.toDouble() * Math.PI / 180)).toFloat() val cY = (centerY + currentRadius1 * sin(i.toDouble() * OUTER_DOTS_POSITION_ANGLE.toDouble() * Math.PI / 180)).toFloat()
canvas.drawCircle(cX, cY, currentDotSize1, circlePaints[i % circlePaints.size]) canvas.drawCircle(cX, cY, currentDotSize1, circlePaints[i % circlePaints.size])
} }
} }
private fun drawInnerDotsFrame(canvas: Canvas) { private fun drawInnerDotsFrame(canvas: Canvas) {
for (i in 0 until DOTS_COUNT) { for (i in 0 until DOTS_COUNT) {
val cX = (centerX + currentRadius2 * Math.cos((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180)).toFloat() val cX = (centerX + currentRadius2 * cos((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180)).toFloat()
val cY = (centerY + currentRadius2 * Math.sin((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180)).toFloat() val cY = (centerY + currentRadius2 * sin((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180)).toFloat()
canvas.drawCircle(cX, cY, currentDotSize2, circlePaints[(i + 1) % circlePaints.size]) canvas.drawCircle(cX, cY, currentDotSize2, circlePaints[(i + 1) % circlePaints.size])
} }
} }

View file

@ -337,7 +337,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
* @param aMyDeviceInfo the device info * @param aMyDeviceInfo the device info
*/ */
private fun refreshCryptographyPreference(devices: List<DeviceInfo>) { private fun refreshCryptographyPreference(devices: List<DeviceInfo>) {
showDeviceListPref.isEnabled = devices.size > 0 showDeviceListPref.isEnabled = devices.isNotEmpty()
showDeviceListPref.summary = resources.getQuantityString(R.plurals.settings_active_sessions_count, devices.size, devices.size) showDeviceListPref.summary = resources.getQuantityString(R.plurals.settings_active_sessions_count, devices.size, devices.size)
// val userId = session.myUserId // val userId = session.myUserId
// val deviceId = session.sessionParams.deviceId // val deviceId = session.sessionParams.deviceId

View file

@ -139,7 +139,7 @@ class ReviewTermsViewModel @AssistedInject constructor(
) )
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "Failed to agree to terms") Timber.e(failure, "Failed to load terms")
setState { setState {
copy( copy(
termsList = Uninitialized termsList = Uninitialized

View file

@ -40,6 +40,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.terms.ReviewTermsActivity import im.vector.riotx.features.terms.ReviewTermsActivity
import im.vector.riotx.features.webview.WebViewEventListener import im.vector.riotx.features.webview.WebViewEventListener
import im.vector.riotx.features.widgets.webview.clearAfterWidget import im.vector.riotx.features.widgets.webview.clearAfterWidget
@ -77,7 +78,7 @@ class WidgetFragment @Inject constructor() : VectorBaseFragment(), WebViewEventL
Timber.v("Observed view events: $it") Timber.v("Observed view events: $it")
when (it) { when (it) {
is WidgetViewEvents.DisplayTerms -> displayTerms(it) is WidgetViewEvents.DisplayTerms -> displayTerms(it)
is WidgetViewEvents.LoadFormattedURL -> loadFormattedUrl(it) is WidgetViewEvents.OnURLFormatted -> loadFormattedUrl(it)
is WidgetViewEvents.DisplayIntegrationManager -> displayIntegrationManager(it) is WidgetViewEvents.DisplayIntegrationManager -> displayIntegrationManager(it)
is WidgetViewEvents.Failure -> displayErrorDialog(it.throwable) is WidgetViewEvents.Failure -> displayErrorDialog(it.throwable)
} }
@ -86,11 +87,17 @@ class WidgetFragment @Inject constructor() : VectorBaseFragment(), WebViewEventL
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == ReviewTermsActivity.TERMS_REQUEST_CODE) { when (requestCode) {
if (resultCode == Activity.RESULT_OK) { ReviewTermsActivity.TERMS_REQUEST_CODE -> {
viewModel.handle(WidgetAction.OnTermsReviewed) Timber.v("On terms results")
} else { if (resultCode == Activity.RESULT_OK) {
vectorBaseActivity.finish() viewModel.handle(WidgetAction.OnTermsReviewed)
} else {
vectorBaseActivity.finish()
}
}
WidgetRequestCodes.INTEGRATION_MANAGER_REQUEST_CODE -> {
viewModel.handle(WidgetAction.LoadFormattedUrl)
} }
} }
} }
@ -139,7 +146,7 @@ class WidgetFragment @Inject constructor() : VectorBaseFragment(), WebViewEventL
override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) { state -> override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) { state ->
when (item.itemId) { when (item.itemId) {
R.id.action_edit -> { R.id.action_edit -> {
navigator.openIntegrationManager(requireContext(), state.roomId, state.widgetId, state.widgetKind.screenId) navigator.openIntegrationManager(this, state.roomId, state.widgetId, state.widgetKind.screenId)
return@withState true return@withState true
} }
R.id.action_delete -> { R.id.action_delete -> {
@ -261,9 +268,9 @@ class WidgetFragment @Inject constructor() : VectorBaseFragment(), WebViewEventL
) )
} }
private fun loadFormattedUrl(loadFormattedUrl: WidgetViewEvents.LoadFormattedURL) { private fun loadFormattedUrl(event: WidgetViewEvents.OnURLFormatted) {
widgetWebView.clearHistory() widgetWebView.clearHistory()
widgetWebView.loadUrl(loadFormattedUrl.formattedURL) widgetWebView.loadUrl(event.formattedURL)
} }
private fun setStateError(message: String?) { private fun setStateError(message: String?) {
@ -280,7 +287,7 @@ class WidgetFragment @Inject constructor() : VectorBaseFragment(), WebViewEventL
private fun displayIntegrationManager(event: WidgetViewEvents.DisplayIntegrationManager) { private fun displayIntegrationManager(event: WidgetViewEvents.DisplayIntegrationManager) {
navigator.openIntegrationManager( navigator.openIntegrationManager(
context = vectorBaseActivity, fragment = this,
roomId = fragmentArgs.roomId, roomId = fragmentArgs.roomId,
integId = event.integId, integId = event.integId,
screen = event.integType screen = event.integType

View file

@ -39,13 +39,12 @@ import java.util.ArrayList
import java.util.HashMap import java.util.HashMap
class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roomId: String, class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roomId: String,
@Assisted private val navigationCallback: NavigationCallback,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val session: Session) : WidgetPostAPIMediator.Handler { private val session: Session) : WidgetPostAPIMediator.Handler {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
fun create(roomId: String, navigationCallback: NavigationCallback): WidgetPostAPIHandler fun create(roomId: String): WidgetPostAPIHandler
} }
interface NavigationCallback { interface NavigationCallback {
@ -54,31 +53,31 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
fun openIntegrationManager(integId: String?, integType: String?) fun openIntegrationManager(integId: String?, integType: String?)
} }
private val widgetPostAPIMediator = session.widgetService().getWidgetPostAPIMediator()
private val room = session.getRoom(roomId)!! private val room = session.getRoom(roomId)!!
var navigationCallback: NavigationCallback? = null
override fun handleWidgetRequest(eventData: JsonDict): Boolean { override fun handleWidgetRequest(mediator: WidgetPostAPIMediator, eventData: JsonDict): Boolean {
return when (eventData["action"] as String?) { return when (eventData["action"] as String?) {
"integration_manager_open" -> handleIntegrationManagerOpenAction(eventData).run { true } "integration_manager_open" -> handleIntegrationManagerOpenAction(eventData).run { true }
"bot_options" -> getBotOptions(eventData).run { true } "bot_options" -> getBotOptions(mediator, eventData).run { true }
"can_send_event" -> canSendEvent(eventData).run { true } "can_send_event" -> canSendEvent(mediator, eventData).run { true }
"close_scalar" -> handleCloseScalar().run { true } "close_scalar" -> handleCloseScalar().run { true }
"get_membership_count" -> getMembershipCount(eventData).run { true } "get_membership_count" -> getMembershipCount(mediator, eventData).run { true }
"get_widgets" -> getWidgets(eventData).run { true } "get_widgets" -> getWidgets(mediator, eventData).run { true }
"invite" -> inviteUser(eventData).run { true } "invite" -> inviteUser(mediator, eventData).run { true }
"join_rules_state" -> getJoinRules(eventData).run { true } "join_rules_state" -> getJoinRules(mediator, eventData).run { true }
"membership_state" -> getMembershipState(eventData).run { true } "membership_state" -> getMembershipState(mediator, eventData).run { true }
"set_bot_options" -> setBotOptions(eventData).run { true } "set_bot_options" -> setBotOptions(mediator, eventData).run { true }
"set_bot_power" -> setBotPower(eventData).run { true } "set_bot_power" -> setBotPower(mediator, eventData).run { true }
"set_plumbing_state" -> setPlumbingState(eventData).run { true } "set_plumbing_state" -> setPlumbingState(mediator, eventData).run { true }
"set_widget" -> setWidget(eventData).run { true } "set_widget" -> setWidget(mediator, eventData).run { true }
"m.sticker" -> pickStickerData(eventData).run { true } "m.sticker" -> pickStickerData(mediator, eventData).run { true }
else -> false else -> false
} }
} }
private fun handleCloseScalar() { private fun handleCloseScalar() {
navigationCallback.close() navigationCallback?.close()
} }
private fun handleIntegrationManagerOpenAction(eventData: JsonDict) { private fun handleIntegrationManagerOpenAction(eventData: JsonDict) {
@ -101,7 +100,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
// Add "type_" as a prefix // Add "type_" as a prefix
integType?.let { integType = "type_$integType" } integType?.let { integType = "type_$integType" }
} }
navigationCallback.openIntegrationManager(integId, integType) navigationCallback?.openIntegrationManager(integId, integType)
} }
/** /**
@ -109,8 +108,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
* *
* @param eventData the modular data * @param eventData the modular data
*/ */
private fun getBotOptions(eventData: JsonDict) { private fun getBotOptions(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) {
if (checkRoomId(eventData) || checkUserId(eventData)) { if (checkRoomId(widgetPostAPIMediator, eventData) || checkUserId(widgetPostAPIMediator, eventData)) {
return return
} }
val userId = eventData["user_id"] as String val userId = eventData["user_id"] as String
@ -134,8 +133,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
} }
} }
private fun canSendEvent(eventData: JsonDict) { private fun canSendEvent(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) {
if (checkRoomId(eventData)) { if (checkRoomId(widgetPostAPIMediator, eventData)) {
return return
} }
Timber.d("Received request canSendEvent in room $roomId") Timber.d("Received request canSendEvent in room $roomId")
@ -170,8 +169,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
* *
* @param eventData the modular data * @param eventData the modular data
*/ */
private fun getMembershipState(eventData: JsonDict) { private fun getMembershipState(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) {
if (checkRoomId(eventData) || checkUserId(eventData)) { if (checkRoomId(widgetPostAPIMediator, eventData) || checkUserId(widgetPostAPIMediator, eventData)) {
return return
} }
val userId = eventData["user_id"] as String val userId = eventData["user_id"] as String
@ -189,8 +188,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
* *
* @param eventData the modular data * @param eventData the modular data
*/ */
private fun getJoinRules(eventData: JsonDict) { private fun getJoinRules(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) {
if (checkRoomId(eventData)) { if (checkRoomId(widgetPostAPIMediator, eventData)) {
return return
} }
Timber.d("Received request join rules in room $roomId") Timber.d("Received request join rules in room $roomId")
@ -207,8 +206,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
* *
* @param eventData the modular data * @param eventData the modular data
*/ */
private fun getWidgets(eventData: JsonDict) { private fun getWidgets(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) {
if (checkRoomId(eventData)) { if (checkRoomId(widgetPostAPIMediator, eventData)) {
return return
} }
Timber.d("Received request to get widget in room $roomId") Timber.d("Received request to get widget in room $roomId")
@ -227,12 +226,12 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
* *
* @param eventData the modular data * @param eventData the modular data
*/ */
private fun setWidget(eventData: JsonDict) { private fun setWidget(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) {
val userWidget = eventData["userWidget"] as Boolean? val userWidget = eventData["userWidget"] as Boolean?
if (userWidget == true) { if (userWidget == true) {
Timber.d("Received request to set widget for user") Timber.d("Received request to set widget for user")
} else { } else {
if (checkRoomId(eventData)) { if (checkRoomId(widgetPostAPIMediator, eventData)) {
return return
} }
Timber.d("Received request to set widget in room $roomId") Timber.d("Received request to set widget in room $roomId")
@ -283,14 +282,14 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
session.updateAccountData( session.updateAccountData(
type = UserAccountData.TYPE_WIDGETS, type = UserAccountData.TYPE_WIDGETS,
content = addUserWidgetBody, content = addUserWidgetBody,
callback = createWidgetAPICallback(eventData) callback = createWidgetAPICallback(widgetPostAPIMediator, eventData)
) )
} else { } else {
session.widgetService().createRoomWidget( session.widgetService().createRoomWidget(
roomId = roomId, roomId = roomId,
widgetId = widgetId, widgetId = widgetId,
content = widgetEventContent, content = widgetEventContent,
callback = createWidgetAPICallback(eventData) callback = createWidgetAPICallback(widgetPostAPIMediator, eventData)
) )
} }
} }
@ -300,8 +299,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
* *
* @param eventData the modular data * @param eventData the modular data
*/ */
private fun setPlumbingState(eventData: JsonDict) { private fun setPlumbingState(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) {
if (checkRoomId(eventData)) { if (checkRoomId(widgetPostAPIMediator, eventData)) {
return return
} }
val description = "Received request to set plumbing state to status " + eventData["status"] + " in room " + roomId + " requested" val description = "Received request to set plumbing state to status " + eventData["status"] + " in room " + roomId + " requested"
@ -315,7 +314,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
eventType = EventType.PLUMBING, eventType = EventType.PLUMBING,
stateKey = null, stateKey = null,
body = params, body = params,
callback = createWidgetAPICallback(eventData) callback = createWidgetAPICallback(widgetPostAPIMediator, eventData)
) )
} }
@ -325,8 +324,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
* @param eventData the modular data * @param eventData the modular data
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private fun setBotOptions(eventData: JsonDict) { private fun setBotOptions(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) {
if (checkRoomId(eventData) || checkUserId(eventData)) { if (checkRoomId(widgetPostAPIMediator, eventData) || checkUserId(widgetPostAPIMediator, eventData)) {
return return
} }
val userId = eventData["user_id"] as String val userId = eventData["user_id"] as String
@ -338,7 +337,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
eventType = EventType.BOT_OPTIONS, eventType = EventType.BOT_OPTIONS,
stateKey = stateKey, stateKey = stateKey,
body = content, body = content,
callback = createWidgetAPICallback(eventData) callback = createWidgetAPICallback(widgetPostAPIMediator, eventData)
) )
} }
@ -347,8 +346,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
* *
* @param eventData the modular data * @param eventData the modular data
*/ */
private fun setBotPower(eventData: JsonDict) { private fun setBotPower(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) {
if (checkRoomId(eventData) || checkUserId(eventData)) { if (checkRoomId(widgetPostAPIMediator, eventData) || checkUserId(widgetPostAPIMediator, eventData)) {
return return
} }
val userId = eventData["user_id"] as String val userId = eventData["user_id"] as String
@ -369,8 +368,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
* *
* @param eventData the modular data * @param eventData the modular data
*/ */
private fun inviteUser(eventData: JsonDict) { private fun inviteUser(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) {
if (checkRoomId(eventData) || checkUserId(eventData)) { if (checkRoomId(widgetPostAPIMediator, eventData) || checkUserId(widgetPostAPIMediator, eventData)) {
return return
} }
val userId = eventData["user_id"] as String val userId = eventData["user_id"] as String
@ -380,7 +379,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
if (member != null && member.membership == Membership.JOIN) { if (member != null && member.membership == Membership.JOIN) {
widgetPostAPIMediator.sendSuccess(eventData) widgetPostAPIMediator.sendSuccess(eventData)
} else { } else {
room.invite(userId = userId, callback = createWidgetAPICallback(eventData)) room.invite(userId = userId, callback = createWidgetAPICallback(widgetPostAPIMediator, eventData))
} }
} }
@ -389,8 +388,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
* *
* @param eventData the modular data * @param eventData the modular data
*/ */
private fun getMembershipCount(eventData: JsonDict) { private fun getMembershipCount(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) {
if (checkRoomId(eventData)) { if (checkRoomId(widgetPostAPIMediator, eventData)) {
return return
} }
val numberOfJoinedMembers = room.getNumberOfJoinedMembers() val numberOfJoinedMembers = room.getNumberOfJoinedMembers()
@ -398,7 +397,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private fun pickStickerData(eventData: JsonDict) { private fun pickStickerData(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict) {
Timber.d("Received request send sticker") Timber.d("Received request send sticker")
val data = eventData["data"] val data = eventData["data"]
if (data == null) { if (data == null) {
@ -411,7 +410,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
return return
} }
widgetPostAPIMediator.sendSuccess(eventData) widgetPostAPIMediator.sendSuccess(eventData)
navigationCallback.closeWithResult(content) navigationCallback?.closeWithResult(content)
} }
/** /**
@ -420,7 +419,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
* *
* @return true in case of error * @return true in case of error
*/ */
private fun checkRoomId(eventData: JsonDict): Boolean { private fun checkRoomId(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict): Boolean {
val roomIdInEvent = eventData["room_id"] as String? val roomIdInEvent = eventData["room_id"] as String?
// Check if param is present // Check if param is present
if (null == roomIdInEvent) { if (null == roomIdInEvent) {
@ -443,7 +442,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
* *
* @return true in case of error * @return true in case of error
*/ */
private fun checkUserId(eventData: JsonDict): Boolean { private fun checkUserId(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict): Boolean {
val userIdInEvent = eventData["user_id"] as String? val userIdInEvent = eventData["user_id"] as String?
// Check if param is present // Check if param is present
if (null == userIdInEvent) { if (null == userIdInEvent) {
@ -454,7 +453,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
return false return false
} }
private fun createWidgetAPICallback(eventData: JsonDict): WidgetAPICallback { private fun createWidgetAPICallback(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict): WidgetAPICallback {
return WidgetAPICallback(widgetPostAPIMediator, eventData, stringProvider) return WidgetAPICallback(widgetPostAPIMediator, eventData, stringProvider)
} }
} }

View file

@ -23,6 +23,6 @@ sealed class WidgetViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable): WidgetViewEvents() data class Failure(val throwable: Throwable): WidgetViewEvents()
data class Close(val content: Content? = null) : WidgetViewEvents() data class Close(val content: Content? = null) : WidgetViewEvents()
data class DisplayIntegrationManager(val integId: String?, val integType: String?) : WidgetViewEvents() data class DisplayIntegrationManager(val integId: String?, val integType: String?) : WidgetViewEvents()
data class LoadFormattedURL(val formattedURL: String) : WidgetViewEvents() data class OnURLFormatted(val formattedURL: String) : WidgetViewEvents()
data class DisplayTerms(val url: String, val token: String) : WidgetViewEvents() data class DisplayTerms(val url: String, val token: String) : WidgetViewEvents()
} }

View file

@ -76,13 +76,22 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi
private val integrationManagerService = session.integrationManagerService() private val integrationManagerService = session.integrationManagerService()
private val widgetURLFormatter = widgetService.getWidgetURLFormatter() private val widgetURLFormatter = widgetService.getWidgetURLFormatter()
private val postAPIMediator = widgetService.getWidgetPostAPIMediator() private val postAPIMediator = widgetService.getWidgetPostAPIMediator()
private var widgetPostAPIHandler: WidgetPostAPIHandler? = null
// Flag to avoid infinite loop
private var canRefreshToken = true
init { init {
integrationManagerService.addListener(this) integrationManagerService.addListener(this)
if (initialState.widgetKind.isAdmin()) { if (initialState.widgetKind.isAdmin()) {
val widgetPostAPIHandler = widgetPostAPIHandlerFactory.create(initialState.roomId, this) widgetPostAPIHandler = widgetPostAPIHandlerFactory.create(initialState.roomId).apply {
navigationCallback = this@WidgetViewModel
}
postAPIMediator.setHandler(widgetPostAPIHandler) postAPIMediator.setHandler(widgetPostAPIHandler)
} }
if (!integrationManagerService.isIntegrationEnabled()) {
_viewEvents.post(WidgetViewEvents.Close(null))
}
setupName() setupName()
refreshPermissionStatus() refreshPermissionStatus()
observePowerLevel() observePowerLevel()
@ -139,10 +148,10 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi
is WidgetAction.OnWebViewLoadingError -> handleWebViewLoadingError(action) is WidgetAction.OnWebViewLoadingError -> handleWebViewLoadingError(action)
is WidgetAction.OnWebViewLoadingSuccess -> handleWebViewLoadingSuccess(action) is WidgetAction.OnWebViewLoadingSuccess -> handleWebViewLoadingSuccess(action)
is WidgetAction.OnWebViewStartedToLoad -> handleWebViewStartLoading() is WidgetAction.OnWebViewStartedToLoad -> handleWebViewStartLoading()
WidgetAction.LoadFormattedUrl -> loadFormattedUrl() WidgetAction.LoadFormattedUrl -> loadFormattedUrl(forceFetchToken = false)
WidgetAction.DeleteWidget -> handleDeleteWidget() WidgetAction.DeleteWidget -> handleDeleteWidget()
WidgetAction.RevokeWidget -> handleRevokeWidget() WidgetAction.RevokeWidget -> handleRevokeWidget()
WidgetAction.OnTermsReviewed -> refreshPermissionStatus() WidgetAction.OnTermsReviewed -> loadFormattedUrl(forceFetchToken = false)
} }
} }
@ -224,10 +233,10 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi
) )
setState { copy(formattedURL = Success(formattedUrl)) } setState { copy(formattedURL = Success(formattedUrl)) }
Timber.v("Post load formatted url event: $formattedUrl") Timber.v("Post load formatted url event: $formattedUrl")
_viewEvents.post(WidgetViewEvents.LoadFormattedURL(formattedUrl)) _viewEvents.post(WidgetViewEvents.OnURLFormatted(formattedUrl))
} catch (failure: Throwable) { } catch (failure: Throwable) {
if (failure is WidgetManagementFailure.TermsNotSignedException) { if (failure is WidgetManagementFailure.TermsNotSignedException) {
_viewEvents.post(WidgetViewEvents.DisplayTerms(failure.baseUrl, failure.token)) _viewEvents.post(WidgetViewEvents.DisplayTerms(initialState.baseUrl, failure.token))
} }
setState { copy(formattedURL = Fail(failure)) } setState { copy(formattedURL = Fail(failure)) }
} }
@ -251,7 +260,8 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi
} }
if (action.isHttpError) { if (action.isHttpError) {
// In case of 403, try to refresh the scalar token // In case of 403, try to refresh the scalar token
if (it.formattedURL is Success && action.errorCode == HttpsURLConnection.HTTP_FORBIDDEN) { if (it.formattedURL is Success && action.errorCode == HttpsURLConnection.HTTP_FORBIDDEN && canRefreshToken) {
canRefreshToken = false
loadFormattedUrl(true) loadFormattedUrl(true)
} }
} else { } else {
@ -261,17 +271,24 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi
override fun onCleared() { override fun onCleared() {
integrationManagerService.removeListener(this) integrationManagerService.removeListener(this)
widgetPostAPIHandler?.navigationCallback = null
postAPIMediator.setHandler(null) postAPIMediator.setHandler(null)
super.onCleared() super.onCleared()
} }
// IntegrationManagerService.Listener // IntegrationManagerService.Listener
override fun onWidgetPermissionsChanged(widgets: Map<String, Boolean>) { override fun onWidgetPermissionsChanged(widgets: Map<String, Boolean>) {
refreshPermissionStatus() refreshPermissionStatus()
} }
// WidgetPostAPIHandler.NavigationCallback override fun onIsEnabledChanged(enabled: Boolean) {
if (!enabled) {
_viewEvents.post(WidgetViewEvents.Close(null))
}
}
// WidgetPostAPIHandler.NavigationCallback
override fun close() { override fun close() {
_viewEvents.post(WidgetViewEvents.Close(null)) _viewEvents.post(WidgetViewEvents.Close(null))

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package im.vector.fragments.roomwidgets package im.vector.riotx.features.widgets.webview
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context

View file

@ -24,7 +24,6 @@ import android.webkit.PermissionRequest
import android.webkit.WebChromeClient import android.webkit.WebChromeClient
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import im.vector.fragments.roomwidgets.WebviewPermissionUtils
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
import im.vector.riotx.features.webview.VectorWebViewClient import im.vector.riotx.features.webview.VectorWebViewClient
@ -81,22 +80,10 @@ fun WebView.clearAfterWidget() {
webChromeClient = null webChromeClient = null
webViewClient = null webViewClient = null
clearHistory() clearHistory()
// NOTE: clears RAM cache, if you pass true, it will also clear the disk cache. // NOTE: clears RAM cache, if you pass true, it will also clear the disk cache.
clearCache(true) clearCache(true)
// Loading a blank page is optional, but will ensure that the WebView isn't doing anything when you destroy it. // Loading a blank page is optional, but will ensure that the WebView isn't doing anything when you destroy it.
loadUrl("about:blank") loadUrl("about:blank")
onPause()
removeAllViews() removeAllViews()
// NOTE: This pauses JavaScript execution for ALL WebViews,
// do not use if you have other WebViews still alive.
// If you create another WebView after calling this,
// make sure to call mWebView.resumeTimers().
pauseTimers()
// NOTE: This can occasionally cause a segfault below API 17 (4.2)
destroy() destroy()
} }

View file

@ -1181,6 +1181,7 @@
<string name="room_widget_webview_read_protected_media">Read DRM protected Media</string> <string name="room_widget_webview_read_protected_media">Read DRM protected Media</string>
<!-- Widget Integration Manager --> <!-- Widget Integration Manager -->
<string name="widget_integration_unable_to_create">Unable to create widget.</string> <string name="widget_integration_unable_to_create">Unable to create widget.</string>
<string name="widget_integration_failed_to_send_request">Failed to send request.</string> <string name="widget_integration_failed_to_send_request">Failed to send request.</string>
<string name="widget_integration_positive_power_level">Power level must be positive integer.</string> <string name="widget_integration_positive_power_level">Power level must be positive integer.</string>