Widget: handle sticker

This commit is contained in:
ganfra 2020-05-26 18:16:38 +02:00
parent dbe4c0c8e4
commit 4b37ede8c2
34 changed files with 384 additions and 277 deletions

View file

@ -18,9 +18,28 @@ package im.vector.matrix.android.api.session.integrationmanager
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager
interface IntegrationManagerService {
interface Listener {
fun onIsEnabledChanged(enabled: Boolean) {
//No-op
}
fun onConfigurationChanged(config: IntegrationManagerConfig) {
//No-op
}
fun onWidgetPermissionsChanged(widgets: Map<String, Boolean>) {
//No-op
}
}
fun addListener(listener: Listener)
fun removeListener(listener: Listener)
fun getOrderedConfigs(): List<IntegrationManagerConfig>
fun getPreferredConfig(): IntegrationManagerConfig

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.room.send
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.OptionItem
@ -28,6 +29,14 @@ import im.vector.matrix.android.api.util.Cancelable
*/
interface SendService {
/**
* Method to send a generic event asynchronously. If you want to send a state event, please use [StateService] instead.
* @param eventType the type of the event
* @param content the optional body as a json dict.
* @return a [Cancelable]
*/
fun sendEvent(eventType: String, content: Content?): Cancelable
/**
* Method to send a text message asynchronously.
* The text to send can be a Spannable and contains special spans (MatrixItemSpan) that will be translated

View file

@ -24,6 +24,14 @@ import javax.inject.Inject
internal class DefaultIntegrationManagerService @Inject constructor(private val integrationManager: IntegrationManager) : IntegrationManagerService {
override fun addListener(listener: IntegrationManagerService.Listener) {
integrationManager.addListener(listener)
}
override fun removeListener(listener: IntegrationManagerService.Listener) {
integrationManager.removeListener(listener)
}
override fun getOrderedConfigs(): List<IntegrationManagerConfig> {
return integrationManager.getOrderedConfigs()
}

View file

@ -23,6 +23,7 @@ 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.integrationmanager.IntegrationManagerService
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
@ -59,27 +60,14 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor:
private val accountDataDataSource: AccountDataDataSource,
private val configExtractor: IntegrationManagerConfigExtractor) {
interface Listener {
fun onIsEnabledChanged(enabled: Boolean) {
//No-op
}
fun onConfigurationChanged(config: IntegrationManagerConfig) {
//No-op
}
fun onWidgetPermissionsChanged(widgets: Map<String, Boolean>) {
//No-op
}
}
private val currentConfigs = ArrayList<IntegrationManagerConfig>()
private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry }
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner)
private val listeners = HashSet<Listener>()
fun addListener(listener: Listener) = synchronized(listeners) { listeners.add(listener) }
fun removeListener(listener: Listener) = synchronized(listeners) { listeners.remove(listener) }
private val listeners = HashSet<IntegrationManagerService.Listener>()
fun addListener(listener: IntegrationManagerService.Listener) = synchronized(listeners) { listeners.add(listener) }
fun removeListener(listener: IntegrationManagerService.Listener) = synchronized(listeners) { listeners.remove(listener) }
init {
val defaultConfig = IntegrationManagerConfig(

View file

@ -33,6 +33,7 @@ import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.content.UploadContentWorker
@ -67,6 +68,12 @@ internal class DefaultSendService @AssistedInject constructor(
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
override fun sendEvent(eventType: String, content: JsonDict?): Cancelable {
return localEchoEventFactory.createEvent(roomId, eventType, content)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown)
.also { createLocalEcho(it) }

View file

@ -23,6 +23,7 @@ import androidx.exifinterface.media.ExifInterface
import im.vector.matrix.android.R
import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Content
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.LocalEcho
@ -56,6 +57,7 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.extensions.subStringBetween
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
@ -95,7 +97,7 @@ internal class LocalEchoEventFactory @Inject constructor(
return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType)
}
val content = MessageTextContent(msgType = msgType, body = text.toString())
return createEvent(roomId, content)
return createMessageEvent(roomId, content)
}
private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent {
@ -129,7 +131,7 @@ internal class LocalEchoEventFactory @Inject constructor(
text != htmlText && htmlText != "<p>${text.trim()}</p>\n"
fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event {
return createEvent(roomId, textContent.toMessageTextContent(msgType))
return createMessageEvent(roomId, textContent.toMessageTextContent(msgType))
}
fun createReplaceTextEvent(roomId: String,
@ -138,7 +140,7 @@ internal class LocalEchoEventFactory @Inject constructor(
newBodyAutoMarkdown: Boolean,
msgType: String,
compatibilityText: String): Event {
return createEvent(roomId,
return createMessageEvent(roomId,
MessageTextContent(
msgType = msgType,
body = compatibilityText,
@ -153,7 +155,7 @@ internal class LocalEchoEventFactory @Inject constructor(
pollEventId: String,
optionIndex: Int,
optionLabel: String): Event {
return createEvent(roomId,
return createMessageEvent(roomId,
MessagePollResponseContent(
body = optionLabel,
relatesTo = RelationDefaultContent(
@ -175,7 +177,7 @@ internal class LocalEchoEventFactory @Inject constructor(
append(it.value)
}
}
return createEvent(
return createMessageEvent(
roomId,
MessageOptionsContent(
body = compatLabel,
@ -211,7 +213,7 @@ internal class LocalEchoEventFactory @Inject constructor(
//
val replyFallback = buildReplyFallback(body, originalEvent.root.senderId ?: "", newBodyText)
return createEvent(roomId,
return createMessageEvent(roomId,
MessageTextContent(
msgType = msgType,
body = compatibilityText,
@ -280,7 +282,7 @@ internal class LocalEchoEventFactory @Inject constructor(
),
url = attachment.queryUri.toString()
)
return createEvent(roomId, content)
return createMessageEvent(roomId, content)
}
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
@ -316,7 +318,7 @@ internal class LocalEchoEventFactory @Inject constructor(
),
url = attachment.queryUri.toString()
)
return createEvent(roomId, content)
return createMessageEvent(roomId, content)
}
private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event {
@ -329,7 +331,7 @@ internal class LocalEchoEventFactory @Inject constructor(
),
url = attachment.queryUri.toString()
)
return createEvent(roomId, content)
return createMessageEvent(roomId, content)
}
private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event {
@ -342,18 +344,22 @@ internal class LocalEchoEventFactory @Inject constructor(
),
url = attachment.queryUri.toString()
)
return createEvent(roomId, content)
return createMessageEvent(roomId, content)
}
private fun createEvent(roomId: String, content: Any? = null): Event {
private fun createMessageEvent(roomId: String, content: Any? = null): Event {
return createEvent(roomId, EventType.MESSAGE, content.toContent())
}
fun createEvent(roomId: String, type: String, content: Content?): Event {
val localId = LocalEcho.createLocalEchoId()
return Event(
roomId = roomId,
originServerTs = dummyOriginServerTs(),
senderId = userId,
eventId = localId,
type = EventType.MESSAGE,
content = content.toContent(),
type = type,
content = content,
unsignedData = UnsignedData(age = null, transactionId = localId)
)
}
@ -410,7 +416,7 @@ internal class LocalEchoEventFactory @Inject constructor(
formattedBody = replyFormatted,
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
)
return createEvent(roomId, content)
return createMessageEvent(roomId, content)
}
private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.widgets
import im.vector.matrix.android.R
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService
import im.vector.matrix.android.api.session.widgets.WidgetURLFormatter
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager
@ -30,9 +31,9 @@ import javax.inject.Inject
internal class DefaultWidgetURLFormatter @Inject constructor(private val integrationManager: IntegrationManager,
private val getScalarTokenTask: GetScalarTokenTask,
private val stringProvider: StringProvider
) : IntegrationManager.Listener, WidgetURLFormatter {
) : IntegrationManagerService.Listener, WidgetURLFormatter {
private var currentConfig = integrationManager.getPreferredConfig()
private lateinit var currentConfig: IntegrationManagerConfig
private var whiteListedUrls: List<String> = emptyList()
fun start() {
@ -50,7 +51,7 @@ internal class DefaultWidgetURLFormatter @Inject constructor(private val integra
private fun setupWithConfiguration() {
val preferredConfig = integrationManager.getPreferredConfig()
if (currentConfig != preferredConfig) {
if (!this::currentConfig.isInitialized || preferredConfig != currentConfig) {
currentConfig = preferredConfig
val defaultWhiteList = stringProvider.getStringArray(R.array.integrations_widgets_urls).asList()
whiteListedUrls = when (preferredConfig.kind) {

View file

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

View file

@ -21,16 +21,16 @@ import javax.inject.Inject
internal class WidgetDependenciesHolder @Inject constructor(private val integrationManager: IntegrationManager,
private val widgetManager: WidgetManager,
private val widgetURLBuilder: DefaultWidgetURLFormatter) {
private val widgetURLFormatter: DefaultWidgetURLFormatter) {
fun start() {
integrationManager.start()
widgetManager.start()
widgetURLBuilder.start()
widgetURLFormatter.start()
}
fun stop() {
widgetURLBuilder.stop()
widgetURLFormatter.stop()
widgetManager.stop()
integrationManager.stop()
}

View file

@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.events.model.Content
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.integrationmanager.IntegrationManagerService
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.android.api.session.widgets.WidgetService
@ -51,7 +52,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
private val stateEventDataSource: StateEventDataSource,
private val taskExecutor: TaskExecutor,
private val createWidgetTask: CreateWidgetTask,
@UserId private val userId: String) : IntegrationManager.Listener {
@UserId private val userId: String) : IntegrationManagerService.Listener {
private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry }
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner)
@ -114,7 +115,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
}
// widgetEvent.stateKey = widget id
if (widgetEvent.stateKey != null && !widgets.containsKey(widgetEvent.stateKey)) {
val widget = Widget(widgetContent, widgetEvent)
val widget = Widget(widgetContent, widgetEvent, widgetEvent.stateKey)
widgets[widgetEvent.stateKey] = widget
}
}

View file

@ -33,7 +33,7 @@ internal fun UserAccountDataEvent.extractWidgetSequence(): Sequence<Widget> {
if (content == null) {
null
} else {
Widget(content, event)
Widget(content, event, event.stateKey)
}
}
}

View file

@ -102,8 +102,7 @@ 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.admin.AdminWidgetFragment
import im.vector.riotx.features.widgets.room.RoomWidgetFragment
import im.vector.riotx.features.widgets.WidgetFragment
@Module
interface FragmentModule {
@ -515,11 +514,7 @@ interface FragmentModule {
@Binds
@IntoMap
@FragmentKey(RoomWidgetFragment::class)
fun bindRoomWidgetFragment(fragment: RoomWidgetFragment): Fragment
@FragmentKey(WidgetFragment::class)
fun bindWidgetFragment(fragment: WidgetFragment): Fragment
@Binds
@IntoMap
@FragmentKey(AdminWidgetFragment::class)
fun bindAdminWidgetFragment(fragment: AdminWidgetFragment): Fragment
}

View file

@ -17,6 +17,7 @@
package im.vector.riotx.features.attachments
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent
import im.vector.riotx.multipicker.entity.MultiPickerAudioType
import im.vector.riotx.multipicker.entity.MultiPickerBaseType
import im.vector.riotx.multipicker.entity.MultiPickerContactType

View file

@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.platform.VectorViewModelAction
@ -26,6 +27,7 @@ import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomDetailAction : VectorViewModelAction {
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
data class SaveDraft(val draft: String) : RoomDetailAction()
data class SendSticker(val stickerContent: MessageStickerContent) : RoomDetailAction()
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : RoomDetailAction()
data class SendMedia(val attachments: List<ContentAttachmentData>, val compressBeforeSending: Boolean) : RoomDetailAction()
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailAction()
@ -72,4 +74,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class RequestVerification(val userId: String) : RoomDetailAction()
data class ResumeVerification(val transactionId: String, val otherUserId: String?) : RoomDetailAction()
data class ReRequestKeys(val eventId: String) : RoomDetailAction()
object SelectStickerAttachment : RoomDetailAction()
}

View file

@ -26,6 +26,7 @@ import android.os.Bundle
import android.os.Parcelable
import android.text.Spannable
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
@ -65,6 +66,7 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
@ -72,6 +74,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageFormat
import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
@ -131,6 +134,7 @@ import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
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.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.action.EventSharedAction
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
@ -155,6 +159,7 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.themes.ThemeUtils
import im.vector.riotx.features.widgets.WidgetActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.parcel.Parcelize
@ -290,20 +295,47 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.observeViewEvents {
when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG)
is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it)
is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it)
is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it)
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG)
is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it)
is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it)
is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it)
RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager()
is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it)
}.exhaustive
}
}
private fun openStickerPicker(event: RoomDetailViewEvents.OpenStickerPicker) {
navigator.openStickerPicker(this, roomDetailArgs.roomId, event.widget)
}
private fun displayPromptForIntegrationManager() {
// The Sticker picker widget is not installed yet. Propose the user to install it
val builder = AlertDialog.Builder(requireContext())
// Use the builder context
// Use the builder context
val v: View = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_no_sticker_pack, null)
builder
.setView(v)
.setPositiveButton(R.string.yes) { _, _->
// Open integration manager, to the sticker installation page
navigator.openIntegrationManager(
context = requireContext(),
roomId = roomDetailArgs.roomId,
integId = null,
screenId = "type_${StickerPickerConstants.WIDGET_NAME}"
)
}
.setNegativeButton(R.string.no, null)
.show()
}
private fun handleJoinedToAnotherRoom(action: RoomDetailViewEvents.JoinRoomCommandSuccess) {
updateComposerText("")
lockSendButton = false
@ -521,15 +553,19 @@ class RoomDetailFragment @Inject constructor(
val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
when (requestCode) {
AttachmentsPreviewActivity.REQUEST_CODE -> {
AttachmentsPreviewActivity.REQUEST_CODE -> {
val sendData = AttachmentsPreviewActivity.getOutput(data)
val keepOriginalSize = AttachmentsPreviewActivity.getKeepOriginalSize(data)
roomDetailViewModel.handle(RoomDetailAction.SendMedia(sendData, !keepOriginalSize))
}
REACTION_SELECT_REQUEST_CODE -> {
REACTION_SELECT_REQUEST_CODE -> {
val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return
roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction))
}
StickerPickerConstants.STICKER_PICKER_REQUEST_CODE -> {
val content = WidgetActivity.getOutput(data).toModel<MessageStickerContent>() ?: return
roomDetailViewModel.handle(RoomDetailAction.SendSticker(content))
}
}
}
}
@ -1386,7 +1422,7 @@ class RoomDetailFragment @Inject constructor(
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(this)
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(this)
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(this)
AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment)
}.exhaustive
}

View file

@ -17,6 +17,7 @@
package im.vector.riotx.features.home.room.detail
import androidx.annotation.StringRes
import im.vector.matrix.android.internal.session.widgets.Widget
import im.vector.riotx.core.platform.VectorViewEvents
import im.vector.riotx.features.command.Command
import java.io.File
@ -49,6 +50,10 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
abstract class SendMessageResult : RoomDetailViewEvents()
object DisplayPromptForIntegrationManager: RoomDetailViewEvents()
data class OpenStickerPicker(val widget: Widget): RoomDetailViewEvents()
object MessageSent : SendMessageResult()
data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult()
class SlashCommandError(val command: Command) : SendMessageResult()

View file

@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail
import android.net.Uri
import androidx.annotation.IdRes
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
@ -34,6 +35,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isImageMessage
import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
@ -67,6 +69,7 @@ import im.vector.riotx.features.command.CommandParser
import im.vector.riotx.features.command.ParsedCommand
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider
import im.vector.riotx.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.riotx.features.home.room.detail.sticker.StickerPickerActionHandler
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotx.features.home.room.typing.TypingHelper
import im.vector.riotx.features.settings.VectorPreferences
@ -74,6 +77,7 @@ import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.launch
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber
@ -89,7 +93,8 @@ class RoomDetailViewModel @AssistedInject constructor(
private val typingHelper: TypingHelper,
private val rainbowGenerator: RainbowGenerator,
private val session: Session,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
private val stickerPickerActionHandler: StickerPickerActionHandler
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener {
private val room = session.getRoom(initialState.roomId)!!
@ -183,6 +188,7 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
is RoomDetailAction.SendMessage -> handleSendMessage(action)
is RoomDetailAction.SendMedia -> handleSendMedia(action)
is RoomDetailAction.SendSticker -> handleSendSticker(action)
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
@ -214,6 +220,18 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
}
}
private fun handleSendSticker(action: RoomDetailAction.SendSticker) {
room.sendEvent(EventType.STICKER, action.stickerContent.toContent())
}
private fun handleSelectStickerAttachment() {
viewModelScope.launch {
val viewEvent = stickerPickerActionHandler.handle()
_viewEvents.post(viewEvent)
}
}

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.home.room.detail.sticker
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.features.home.room.detail.RoomDetailViewEvents
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class StickerPickerActionHandler @Inject constructor(private val session: Session) {
suspend fun handle(): RoomDetailViewEvents = withContext(Dispatchers.Default) {
// Search for the sticker picker widget in the user account
val stickerWidget = session.widgetService().getUserWidgets(setOf(StickerPickerConstants.WIDGET_NAME)).firstOrNull()
if (stickerWidget == null || stickerWidget.widgetContent.url.isNullOrBlank()) {
RoomDetailViewEvents.DisplayPromptForIntegrationManager
} else {
RoomDetailViewEvents.OpenStickerPicker(
widget = stickerWidget
)
}
}
}

View file

@ -14,8 +14,9 @@
* limitations under the License.
*/
package im.vector.riotx.features.widgets.admin
package im.vector.riotx.features.home.room.detail.sticker
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class AdminWidgetAction : VectorViewModelAction
object StickerPickerConstants {
const val WIDGET_NAME = "m.stickerpicker"
const val STICKER_PICKER_REQUEST_CODE = 16000
}

View file

@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerif
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.util.MatrixItem
import im.vector.matrix.android.internal.session.widgets.Widget
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.error.fatalError
@ -45,6 +46,7 @@ import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import im.vector.riotx.features.debug.DebugMenuActivity
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.sticker.StickerPickerConstants
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity
import im.vector.riotx.features.media.BigImageViewerActivity
@ -66,6 +68,7 @@ import im.vector.riotx.features.widgets.WidgetActivity
import im.vector.riotx.features.widgets.WidgetArgsBuilder
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DefaultNavigator @Inject constructor(
private val sessionHolder: ActiveSessionHolder,
@ -224,6 +227,12 @@ class DefaultNavigator @Inject constructor(
fragment.startActivityForResult(intent, requestCode)
}
override fun openStickerPicker(fragment: Fragment, roomId: String, widget: Widget, requestCode: Int) {
val widgetArgs = widgetArgsBuilder.buildStickerPickerArgs(roomId, widget)
val intent = WidgetActivity.newIntent(fragment.requireContext(), widgetArgs)
fragment.startActivityForResult(intent, StickerPickerConstants.STICKER_PICKER_REQUEST_CODE)
}
override fun openIntegrationManager(context: Context, roomId: String, integId: String?, screenId: String?) {
val widgetArgs = widgetArgsBuilder.buildIntegrationManagerArgs(roomId, integId, screenId)
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))

View file

@ -24,6 +24,8 @@ import androidx.fragment.app.Fragment
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.util.MatrixItem
import im.vector.matrix.android.internal.session.widgets.Widget
import im.vector.riotx.features.home.room.detail.sticker.StickerPickerConstants
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.settings.VectorSettingsActivity
@ -80,6 +82,11 @@ interface Navigator {
token: String?,
requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE)
fun openStickerPicker(fragment: Fragment,
roomId: String,
widget: Widget,
requestCode: Int = StickerPickerConstants.STICKER_PICKER_REQUEST_CODE)
fun openIntegrationManager(context: Context, roomId: String, integId: String?, screenId: String?)
fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?)

View file

@ -14,13 +14,13 @@
* limitations under the License.
*/
package im.vector.riotx.features.widgets.room
package im.vector.riotx.features.widgets
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomWidgetAction : VectorViewModelAction {
data class OnWebViewStartedToLoad(val url: String) : RoomWidgetAction()
data class OnWebViewLoadingError(val url: String, val isHttpError: Boolean, val errorCode: Int, val errorDescription: String) : RoomWidgetAction()
data class OnWebViewLoadingSuccess(val url: String) : RoomWidgetAction()
object OnTermsReviewed: RoomWidgetAction()
sealed class WidgetAction : VectorViewModelAction {
data class OnWebViewStartedToLoad(val url: String) : WidgetAction()
data class OnWebViewLoadingError(val url: String, val isHttpError: Boolean, val errorCode: Int, val errorDescription: String) : WidgetAction()
data class OnWebViewLoadingSuccess(val url: String) : WidgetAction()
object OnTermsReviewed: WidgetAction()
}

View file

@ -19,17 +19,18 @@ package im.vector.riotx.features.widgets
import android.content.Context
import android.content.Intent
import androidx.appcompat.widget.Toolbar
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.riotx.R
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.widgets.room.RoomWidgetFragment
import im.vector.riotx.features.widgets.room.WidgetArgs
import java.io.Serializable
class WidgetActivity : VectorBaseActivity(), ToolbarConfigurable {
companion object {
private const val EXTRA_RESULT = "EXTRA_RESULT"
private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS"
fun newIntent(context: Context, args: WidgetArgs): Intent {
@ -37,6 +38,17 @@ class WidgetActivity : VectorBaseActivity(), ToolbarConfigurable {
putExtra(EXTRA_FRAGMENT_ARGS, args)
}
}
@Suppress("UNCHECKED_CAST")
fun getOutput(intent: Intent): Content? {
return intent.extras?.getSerializable(EXTRA_RESULT) as? Content
}
fun createResultIntent(content: Content): Intent {
return Intent().apply {
putExtra(EXTRA_RESULT, content as Serializable)
}
}
}
override fun getLayoutRes() = R.layout.activity_simple
@ -45,7 +57,7 @@ class WidgetActivity : VectorBaseActivity(), ToolbarConfigurable {
if (isFirstCreation()) {
val fragmentArgs: WidgetArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS)
?: return
addFragment(R.id.simpleFragmentContainer, RoomWidgetFragment::class.java, fragmentArgs)
addFragment(R.id.simpleFragmentContainer, WidgetFragment::class.java, fragmentArgs)
}
}

View file

@ -16,9 +16,8 @@
package im.vector.riotx.features.widgets
import im.vector.matrix.android.internal.session.widgets.Widget
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.features.widgets.room.WidgetArgs
import im.vector.riotx.features.widgets.room.WidgetKind
import javax.inject.Inject
class WidgetArgsBuilder @Inject constructor(private val sessionHolder: ActiveSessionHolder) {
@ -35,7 +34,28 @@ class WidgetArgsBuilder @Inject constructor(private val sessionHolder: ActiveSes
"screen" to screenId,
"integ_id" to integId,
"room_id" to roomId
).filterValues { it != null } as Map<String, String>
).filterNotNull()
)
}
@Suppress("UNCHECKED_CAST")
fun buildStickerPickerArgs(roomId: String, widget: Widget): WidgetArgs {
val widgetId = widget.widgetId
val baseUrl = widget.widgetContent.url ?: throw IllegalStateException()
return WidgetArgs(
baseUrl = baseUrl,
kind = WidgetKind.USER,
roomId = roomId,
widgetId = widgetId,
urlParams = mapOf(
"widgetId" to widgetId,
"room_id" to roomId
).filterNotNull()
)
}
@Suppress("UNCHECKED_CAST")
private fun Map<String, String?>.filterNotNull(): Map<String, String>{
return filterValues { it != null } as Map<String, String>
}
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.features.widgets.room
package im.vector.riotx.features.widgets
import android.app.Activity
import android.content.Intent
@ -51,12 +51,12 @@ data class WidgetArgs(
val urlParams: Map<String, String> = emptyMap()
) : Parcelable
class RoomWidgetFragment @Inject constructor(
private val viewModelFactory: RoomWidgetViewModel.Factory
) : VectorBaseFragment(), RoomWidgetViewModel.Factory by viewModelFactory, WebViewEventListener {
class WidgetFragment @Inject constructor(
private val viewModelFactory: WidgetViewModel.Factory
) : VectorBaseFragment(), WidgetViewModel.Factory by viewModelFactory, WebViewEventListener {
private val fragmentArgs: WidgetArgs by args()
private val viewModel: RoomWidgetViewModel by fragmentViewModel()
private val viewModel: WidgetViewModel by fragmentViewModel()
override fun getLayoutResId() = R.layout.fragment_room_widget
@ -68,15 +68,10 @@ class RoomWidgetFragment @Inject constructor(
}
viewModel.observeViewEvents {
when (it) {
is RoomWidgetViewEvents.DisplayTerms -> displayTerms(it)
is RoomWidgetViewEvents.LoadFormattedURL -> loadFormattedUrl(it)
is RoomWidgetViewEvents.Close -> vectorBaseActivity.finish()
is RoomWidgetViewEvents.DisplayIntegrationManager -> navigator.openIntegrationManager(
context = vectorBaseActivity,
roomId = fragmentArgs.roomId,
integId = it.integId,
screenId = it.integType
)
is WidgetViewEvents.DisplayTerms -> displayTerms(it)
is WidgetViewEvents.LoadFormattedURL -> loadFormattedUrl(it)
is WidgetViewEvents.Close -> handleClose(it)
is WidgetViewEvents.DisplayIntegrationManager -> displayIntegrationManager(it)
}
}
}
@ -84,7 +79,7 @@ class RoomWidgetFragment @Inject constructor(
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == ReviewTermsActivity.TERMS_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
viewModel.handle(RoomWidgetAction.OnTermsReviewed)
viewModel.handle(WidgetAction.OnTermsReviewed)
} else {
vectorBaseActivity.finish()
}
@ -169,7 +164,23 @@ class RoomWidgetFragment @Inject constructor(
}
}
private fun displayTerms(displayTerms: RoomWidgetViewEvents.DisplayTerms) {
override fun onPageStarted(url: String) {
viewModel.handle(WidgetAction.OnWebViewStartedToLoad(url))
}
override fun onPageFinished(url: String) {
viewModel.handle(WidgetAction.OnWebViewLoadingSuccess(url))
}
override fun onPageError(url: String, errorCode: Int, description: String) {
viewModel.handle(WidgetAction.OnWebViewLoadingError(url, false, errorCode, description))
}
override fun onHttpError(url: String, errorCode: Int, description: String) {
viewModel.handle(WidgetAction.OnWebViewLoadingError(url, true, errorCode, description))
}
private fun displayTerms(displayTerms: WidgetViewEvents.DisplayTerms) {
navigator.openTerms(
fragment = this,
serviceType = TermsService.ServiceType.IntegrationManager,
@ -178,7 +189,7 @@ class RoomWidgetFragment @Inject constructor(
)
}
private fun loadFormattedUrl(loadFormattedUrl: RoomWidgetViewEvents.LoadFormattedURL) {
private fun loadFormattedUrl(loadFormattedUrl: WidgetViewEvents.LoadFormattedURL) {
widgetWebView.clearHistory()
widgetWebView.loadUrl(loadFormattedUrl.formattedURL)
}
@ -195,19 +206,20 @@ class RoomWidgetFragment @Inject constructor(
}
}
override fun onPageStarted(url: String) {
viewModel.handle(RoomWidgetAction.OnWebViewStartedToLoad(url))
private fun displayIntegrationManager(event: WidgetViewEvents.DisplayIntegrationManager) {
navigator.openIntegrationManager(
context = vectorBaseActivity,
roomId = fragmentArgs.roomId,
integId = event.integId,
screenId = event.integType
)
}
override fun onPageFinished(url: String) {
viewModel.handle(RoomWidgetAction.OnWebViewLoadingSuccess(url))
}
override fun onPageError(url: String, errorCode: Int, description: String) {
viewModel.handle(RoomWidgetAction.OnWebViewLoadingError(url, false, errorCode, description))
}
override fun onHttpError(url: String, errorCode: Int, description: String) {
viewModel.handle(RoomWidgetAction.OnWebViewLoadingError(url, true, errorCode, description))
private fun handleClose(event: WidgetViewEvents.Close) {
if (event.content != null) {
val intent = WidgetActivity.createResultIntent(event.content)
vectorBaseActivity.setResult(Activity.RESULT_OK, intent)
}
vectorBaseActivity.finish()
}
}

View file

@ -21,6 +21,7 @@ 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.matrix.android.api.session.events.model.Content
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.toContent
@ -49,6 +50,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
interface NavigationCallback {
fun close()
fun closeWithResult(content: Content)
fun openIntegrationManager(integId: String?, integType: String?)
}
@ -70,6 +72,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
"set_bot_power" -> setBotPower(eventData).run { true }
"set_plumbing_state" -> setPlumbingState(eventData).run { true }
"set_widget" -> setWidget(eventData).run { true }
"m.sticker" -> pickStickerData(eventData).run { true }
else -> false
}
}
@ -394,6 +397,23 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
widgetPostAPIMediator.sendIntegerResponse(numberOfJoinedMembers, eventData)
}
@Suppress("UNCHECKED_CAST")
private fun pickStickerData(eventData: JsonDict) {
Timber.d("Received request send sticker")
val data = eventData["data"]
if (data == null) {
widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_missing_parameter), eventData)
return
}
val content = (data as? JsonDict)?.get("content") as? Content
if (content == null) {
widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_missing_parameter), eventData)
return
}
widgetPostAPIMediator.sendSuccess(eventData)
navigationCallback.closeWithResult(content)
}
/**
* Check if roomId is present in the event and match
* Send response and return true in case of error

View file

@ -14,13 +14,14 @@
* limitations under the License.
*/
package im.vector.riotx.features.widgets.room
package im.vector.riotx.features.widgets
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.riotx.core.platform.VectorViewEvents
sealed class RoomWidgetViewEvents : VectorViewEvents {
object Close: RoomWidgetViewEvents()
data class DisplayIntegrationManager(val integId: String?, val integType: String?): RoomWidgetViewEvents()
data class LoadFormattedURL(val formattedURL: String): RoomWidgetViewEvents()
data class DisplayTerms(val url: String, val token: String): RoomWidgetViewEvents()
sealed class WidgetViewEvents : VectorViewEvents {
data class Close(val content: Content?): WidgetViewEvents()
data class DisplayIntegrationManager(val integId: String?, val integType: String?): WidgetViewEvents()
data class LoadFormattedURL(val formattedURL: String): WidgetViewEvents()
data class DisplayTerms(val url: String, val token: String): WidgetViewEvents()
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.features.widgets.room
package im.vector.riotx.features.widgets
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
@ -28,27 +28,30 @@ 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.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService
import im.vector.matrix.android.internal.session.widgets.WidgetManagementFailure
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.widgets.WidgetPostAPIHandler
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.net.ssl.HttpsURLConnection
class RoomWidgetViewModel @AssistedInject constructor(@Assisted val initialState: WidgetViewState,
private val widgetPostAPIHandlerFactory: WidgetPostAPIHandler.Factory,
private val session: Session)
: VectorViewModel<WidgetViewState, RoomWidgetAction, RoomWidgetViewEvents>(initialState), WidgetPostAPIHandler.NavigationCallback {
class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: WidgetViewState,
private val widgetPostAPIHandlerFactory: WidgetPostAPIHandler.Factory,
private val session: Session)
: VectorViewModel<WidgetViewState, WidgetAction, WidgetViewEvents>(initialState),
WidgetPostAPIHandler.NavigationCallback,
IntegrationManagerService.Listener {
@AssistedInject.Factory
interface Factory {
fun create(initialState: WidgetViewState): RoomWidgetViewModel
fun create(initialState: WidgetViewState): WidgetViewModel
}
companion object : MvRxViewModelFactory<RoomWidgetViewModel, WidgetViewState> {
companion object : MvRxViewModelFactory<WidgetViewModel, WidgetViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: WidgetViewState): RoomWidgetViewModel? {
override fun create(viewModelContext: ViewModelContext, state: WidgetViewState): WidgetViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
@ -59,11 +62,12 @@ class RoomWidgetViewModel @AssistedInject constructor(@Assisted val initialState
private val widgetService = session.widgetService()
private val integrationManagerService = session.integrationManagerService()
private val widgetBuilder = widgetService.getWidgetURLFormatter()
private val widgetURLFormatter = widgetService.getWidgetURLFormatter()
private val postAPIMediator = widgetService.getWidgetPostAPIMediator()
init {
if(initialState.widgetKind.isAdmin()) {
integrationManagerService.addListener(this)
if (initialState.widgetKind.isAdmin()) {
val widgetPostAPIHandler = widgetPostAPIHandlerFactory.create(initialState.roomId, this)
postAPIMediator.setHandler(widgetPostAPIHandler)
}
@ -82,11 +86,11 @@ class RoomWidgetViewModel @AssistedInject constructor(@Assisted val initialState
fun getPostAPIMediator() = postAPIMediator
override fun handle(action: RoomWidgetAction) {
override fun handle(action: WidgetAction) {
when (action) {
is RoomWidgetAction.OnWebViewLoadingError -> handleWebViewLoadingError(action.isHttpError, action.errorCode, action.errorDescription)
is RoomWidgetAction.OnWebViewLoadingSuccess -> handleWebViewLoadingSuccess(action.url)
is RoomWidgetAction.OnWebViewStartedToLoad -> handleWebViewStartLoading()
is WidgetAction.OnWebViewLoadingError -> handleWebViewLoadingError(action.isHttpError, action.errorCode, action.errorDescription)
is WidgetAction.OnWebViewLoadingSuccess -> handleWebViewLoadingSuccess(action.url)
is WidgetAction.OnWebViewStartedToLoad -> handleWebViewStartLoading()
}
}
@ -131,17 +135,17 @@ class RoomWidgetViewModel @AssistedInject constructor(@Assisted val initialState
viewModelScope.launch {
try {
setState { copy(formattedURL = Loading()) }
val formattedUrl = widgetBuilder.format(
val formattedUrl = widgetURLFormatter.format(
baseUrl = initialState.baseUrl,
params = initialState.urlParams,
forceFetchScalarToken = forceFetchToken,
bypassWhitelist = initialState.widgetKind == WidgetKind.INTEGRATION_MANAGER
)
setState { copy(formattedURL = Success(formattedUrl)) }
_viewEvents.post(RoomWidgetViewEvents.LoadFormattedURL(formattedUrl))
_viewEvents.post(WidgetViewEvents.LoadFormattedURL(formattedUrl))
} catch (failure: Throwable) {
if (failure is WidgetManagementFailure.TermsNotSignedException) {
_viewEvents.post(RoomWidgetViewEvents.DisplayTerms(failure.baseUrl, failure.token))
_viewEvents.post(WidgetViewEvents.DisplayTerms(failure.baseUrl, failure.token))
}
setState { copy(formattedURL = Fail(failure)) }
}
@ -162,8 +166,10 @@ class RoomWidgetViewModel @AssistedInject constructor(@Assisted val initialState
private fun handleWebViewLoadingError(isHttpError: Boolean, reason: Int, errorDescription: String) {
if (isHttpError) {
// In case of 403, try to refresh the scalar token
if (reason == HttpsURLConnection.HTTP_FORBIDDEN) {
loadFormattedUrl(true)
withState {
if (it.formattedURL is Success && reason == HttpsURLConnection.HTTP_FORBIDDEN) {
loadFormattedUrl(true)
}
}
} else {
setState { copy(webviewLoadedUrl = Fail(Throwable(errorDescription))) }
@ -172,14 +178,27 @@ class RoomWidgetViewModel @AssistedInject constructor(@Assisted val initialState
override fun onCleared() {
super.onCleared()
integrationManagerService.removeListener(this)
postAPIMediator.setHandler(null)
}
// IntegrationManagerService.Listener
override fun onWidgetPermissionsChanged(widgets: Map<String, Boolean>) {
refreshPermissionStatus()
}
// WidgetPostAPIHandler.NavigationCallback
override fun close() {
_viewEvents.post(RoomWidgetViewEvents.Close)
_viewEvents.post(WidgetViewEvents.Close(null))
}
override fun closeWithResult(content: Content) {
_viewEvents.post(WidgetViewEvents.Close(content))
}
override fun openIntegrationManager(integId: String?, integType: String?) {
_viewEvents.post(RoomWidgetViewEvents.DisplayIntegrationManager(integId, integType))
_viewEvents.post(WidgetViewEvents.DisplayIntegrationManager(integId, integType))
}
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.features.widgets.room
package im.vector.riotx.features.widgets
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState

View file

@ -1,49 +0,0 @@
/*
* 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

@ -1,21 +0,0 @@
/*
* 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 im.vector.riotx.core.platform.VectorViewEvents
sealed class AdminWidgetViewEvents : VectorViewEvents

View file

@ -1,50 +0,0 @@
/*
* 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.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.riotx.core.platform.VectorViewModel
class AdminWidgetViewModel @AssistedInject constructor(@Assisted initialState: AdminWidgetViewState)
: VectorViewModel<AdminWidgetViewState, AdminWidgetAction, AdminWidgetViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: AdminWidgetViewState): AdminWidgetViewModel
}
companion object : MvRxViewModelFactory<AdminWidgetViewModel, AdminWidgetViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: AdminWidgetViewState): AdminWidgetViewModel? {
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")
}
}
override fun handle(action: AdminWidgetAction) {
}
}

View file

@ -1,21 +0,0 @@
/*
* 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,10 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawableBottom="@drawable/stickerpack_rabbit"
android:drawablePadding="16dp"
android:gravity="center_horizontal"
android:maxWidth="300dp"
android:paddingTop="16dp"
android:text="@string/no_sticker_application_dialog_content" />