Widget: add active widgets

This commit is contained in:
ganfra 2020-05-28 17:08:57 +02:00
parent 1fe0c8a3e9
commit cb80d8d349
20 changed files with 162 additions and 69 deletions

View file

@ -98,7 +98,7 @@ data class Event(
* @return true if event is state event.
*/
fun isStateEvent(): Boolean {
return EventType.isStateEvent(getClearType())
return stateKey != null
}
// ==============================================================================================================

View file

@ -38,6 +38,8 @@ object EventType {
// State Events
const val STATE_ROOM_WIDGET_LEGACY = "im.vector.modular.widgets"
const val STATE_ROOM_WIDGET = "m.widget"
const val STATE_ROOM_NAME = "m.room.name"
const val STATE_ROOM_TOPIC = "m.room.topic"
const val STATE_ROOM_AVATAR = "m.room.avatar"
@ -84,29 +86,6 @@ object EventType {
// Unwedging
internal const val DUMMY = "m.dummy"
private val STATE_EVENTS = listOf(
STATE_ROOM_NAME,
STATE_ROOM_TOPIC,
STATE_ROOM_AVATAR,
STATE_ROOM_MEMBER,
STATE_ROOM_THIRD_PARTY_INVITE,
STATE_ROOM_CREATE,
STATE_ROOM_JOIN_RULES,
STATE_ROOM_GUEST_ACCESS,
STATE_ROOM_POWER_LEVELS,
STATE_ROOM_ALIASES,
STATE_ROOM_TOMBSTONE,
STATE_ROOM_CANONICAL_ALIAS,
STATE_ROOM_HISTORY_VISIBILITY,
STATE_ROOM_RELATED_GROUPS,
STATE_ROOM_PINNED_EVENT,
STATE_ROOM_ENCRYPTION
)
fun isStateEvent(type: String): Boolean {
return STATE_EVENTS.contains(type)
}
fun isCallEvent(type: String): Boolean {
return type == CALL_INVITE
|| type == CALL_CANDIDATES

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.api.session.room.powerlevels
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
/**
@ -44,11 +43,11 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @param userId the user id
* @return true if the user can send this type of event
*/
fun isAllowedToSend(eventType: String, userId: String): Boolean {
return if (eventType.isNotEmpty() && userId.isNotEmpty()) {
fun isAllowedToSend(isState: Boolean, eventType: String?, userId: String): Boolean {
return if (userId.isNotEmpty()) {
val powerLevel = getUserPowerLevel(userId)
val minimumPowerLevel = powerLevelsContent.events[eventType]
?: if (EventType.isStateEvent(eventType)) {
?: if (isState) {
powerLevelsContent.stateDefault
} else {
powerLevelsContent.eventsDefault
@ -57,25 +56,6 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
} else false
}
/**
* Tell if an user can send an event of a certain type
*
* @param eventType the event type to check for
* @param userId the user id
* @return true if the user can send this type of event
*/
fun isAllowedToSend(isState: Boolean, userId: String): Boolean {
return if (userId.isNotEmpty()) {
val powerLevel = getUserPowerLevel(userId)
val minimumPowerLevel = if (isState) {
powerLevelsContent.stateDefault
} else {
powerLevelsContent.eventsDefault
}
powerLevel >= minimumPowerLevel
} else false
}
/**
* Get the notification level for a dedicated key.
*

View file

@ -20,15 +20,12 @@ import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.session.widgets.Widget
interface WidgetService {
companion object {
const val WIDGET_EVENT_TYPE = "im.vector.modular.widgets"
}
fun getWidgetURLFormatter(): WidgetURLFormatter
fun getWidgetPostAPIMediator(): WidgetPostAPIMediator

View file

@ -240,7 +240,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
eventIds.add(event.eventId)
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm)
if (event.isStateEvent() && event.stateKey != null) {
if (event.stateKey != null) {
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
eventId = event.eventId
root = eventEntity

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.widgets
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.widgets.WidgetService
import im.vector.matrix.android.internal.database.awaitNotEmptyResult
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
@ -48,14 +49,14 @@ internal class DefaultCreateWidgetTask @Inject constructor(private val monarchy:
executeRequest<Unit>(eventBus) {
apiCall = roomAPI.sendStateEvent(
roomId = params.roomId,
stateEventType = WidgetService.WIDGET_EVENT_TYPE,
stateEventType = EventType.STATE_ROOM_WIDGET_LEGACY,
stateKey = params.widgetId,
params = params.content
)
}
awaitNotEmptyResult(monarchy.realmConfiguration, 30_000L) {
CurrentStateEventEntity
.whereStateKey(it, params.roomId, type = WidgetService.WIDGET_EVENT_TYPE, stateKey = params.widgetId)
.whereStateKey(it, params.roomId, type = EventType.STATE_ROOM_WIDGET_LEGACY, stateKey = params.widgetId)
.and()
.equalTo(CurrentStateEventEntityFields.ROOT.SENDER, userId)
}

View file

@ -30,7 +30,6 @@ 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
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.SessionScope
@ -75,7 +74,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
excludedTypes: Set<String>? = null
): LiveData<List<Widget>> {
// Get all im.vector.modular.widgets state events in the room
val liveWidgetEvents = stateEventDataSource.getStateEventsLive(roomId, setOf(WidgetService.WIDGET_EVENT_TYPE), widgetId)
val liveWidgetEvents = stateEventDataSource.getStateEventsLive(roomId, setOf(EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_WIDGET_LEGACY), widgetId)
return Transformations.map(liveWidgetEvents) { widgetEvents ->
widgetEvents.mapEventsToWidgets(widgetTypes, excludedTypes)
}
@ -88,7 +87,7 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
excludedTypes: Set<String>? = null
): List<Widget> {
// Get all im.vector.modular.widgets state events in the room
val widgetEvents: List<Event> = stateEventDataSource.getStateEvents(roomId, setOf(WidgetService.WIDGET_EVENT_TYPE), widgetId)
val widgetEvents: List<Event> = stateEventDataSource.getStateEvents(roomId, setOf(EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_WIDGET_LEGACY), widgetId)
return widgetEvents.mapEventsToWidgets(widgetTypes, excludedTypes)
}
@ -185,6 +184,6 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
fun hasPermissionsToHandleWidgets(roomId: String): Boolean {
val powerLevelsEvent = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false
return PowerLevelsHelper(powerLevelsContent).isAllowedToSend(EventType.STATE_ROOM_POWER_LEVELS, userId)
return PowerLevelsHelper(powerLevelsContent).isAllowedToSend(true, null, userId)
}
}

View file

@ -147,6 +147,7 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformatio
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem
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.widget.RoomWidgetsBannerView
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView
@ -199,7 +200,7 @@ class RoomDetailFragment @Inject constructor(
VectorInviteView.Callback,
JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback,
AttachmentsHelper.Callback {
AttachmentsHelper.Callback, RoomWidgetsBannerView.Callback {
companion object {
@ -264,6 +265,8 @@ class RoomDetailFragment @Inject constructor(
setupNotificationView()
setupJumpToReadMarkerView()
setupJumpToBottomView()
setupWidgetsBannerView()
roomToolbarContentView.debouncedClicks {
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
}
@ -311,6 +314,10 @@ class RoomDetailFragment @Inject constructor(
}
}
private fun setupWidgetsBannerView() {
roomWidgetsBannerView.callback = this
}
private fun openStickerPicker(event: RoomDetailViewEvents.OpenStickerPicker) {
navigator.openStickerPicker(this, roomDetailArgs.roomId, event.widget)
}
@ -323,7 +330,7 @@ class RoomDetailFragment @Inject constructor(
val v: View = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_no_sticker_pack, null)
builder
.setView(v)
.setPositiveButton(R.string.yes) { _, _->
.setPositiveButton(R.string.yes) { _, _ ->
// Open integration manager, to the sticker installation page
navigator.openIntegrationManager(
context = requireContext(),
@ -719,6 +726,7 @@ class RoomDetailFragment @Inject constructor(
val summary = state.asyncRoomSummary()
val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) {
roomWidgetsBannerView.render(state.activeRoomWidgets())
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
timelineEventController.update(state)
inviteView.visibility = View.GONE
@ -1455,4 +1463,8 @@ class RoomDetailFragment @Inject constructor(
val formattedContact = contactAttachment.toHumanReadable()
roomDetailViewModel.handle(RoomDetailAction.SendMessage(formattedContact, false))
}
override fun onViewWidgetsClicked() {
Toast.makeText(requireContext(), "Show widgets", Toast.LENGTH_SHORT).show()
}
}

View file

@ -158,7 +158,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
}
}
} else {
if (EventType.isStateEvent(event.root.type)) {
if (event.root.isStateEvent()) {
// Do not warn for state event, they are always in clear
E2EDecoration.NONE
} else {

View file

@ -0,0 +1,59 @@
/*
* 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.widget
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.RelativeLayout
import im.vector.matrix.android.internal.session.widgets.Widget
import im.vector.riotx.R
import kotlinx.android.synthetic.main.view_room_widgets_banner.view.*
class RoomWidgetsBannerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
interface Callback {
fun onViewWidgetsClicked()
}
var callback: Callback? = null
init {
setupView()
}
private fun setupView() {
inflate(context, R.layout.view_room_widgets_banner, this)
setBackgroundResource(R.drawable.bg_active_widgets_banner)
setOnClickListener {
callback?.onViewWidgetsClicked()
}
}
fun render(widgets: List<Widget>?) {
if (widgets.isNullOrEmpty()) {
visibility = View.GONE
} else {
visibility = View.VISIBLE
activeWidgetsLabel.text = context.resources.getQuantityString(R.plurals.active_widgets, widgets.size, widgets.size)
}
}
}

View file

@ -154,7 +154,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
val canSend = if (powerLevelsContent == null) {
false
} else {
PowerLevelsHelper(powerLevelsContent).isAllowedToSend(eventType, session.myUserId)
PowerLevelsHelper(powerLevelsContent).isAllowedToSend(isState, eventType, session.myUserId)
}
if (canSend) {
Timber.d("## canSendEvent() returns true")

View file

@ -92,7 +92,7 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi
}
private fun subscribeToWidget() {
asyncSubscribe(WidgetViewState::asyncWidget){
asyncSubscribe(WidgetViewState::asyncWidget) {
setState { copy(widgetName = it.name) }
}
}
@ -113,7 +113,7 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi
.mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap()
.map {
PowerLevelsHelper(it).isAllowedToSend(true, session.myUserId)
PowerLevelsHelper(it).isAllowedToSend(true, null, session.myUserId)
}.subscribe {
setState { copy(canManageWidgets = it) }
}.disposeOnClear()

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<solid android:color="?attr/riotx_room_active_widgets_banner" />
</shape>

View file

@ -108,15 +108,31 @@
app:layout_constraintTop_toBottomOf="@id/syncStateView"
tools:listitem="@layout/item_timeline_event_base" />
<im.vector.riotx.core.ui.views.JumpToReadMarkerView
android:id="@+id/jumpToReadMarkerView"
<FrameLayout
android:id="@+id/bannersContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/syncStateView"
tools:visibility="visible" />
app:layout_constraintTop_toBottomOf="@id/syncStateView">
<im.vector.riotx.features.home.room.detail.widget.RoomWidgetsBannerView
android:id="@+id/roomWidgetsBannerView"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<im.vector.riotx.core.ui.views.JumpToReadMarkerView
android:id="@+id/jumpToReadMarkerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible"
tools:visibility="visible" />
</FrameLayout>
<im.vector.riotx.core.ui.views.NotificationAreaView
android:id="@+id/notificationAreaView"

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_active_widgets_banner"
tools:parentTag="android.widget.RelativeLayout">
<TextView
android:id="@+id/activeWidgetsLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingTop="8dp"
android:paddingEnd="12dp"
android:paddingBottom="8dp"
android:background="?attr/selectableItemBackground"
android:textColor="?riotx_text_primary_body_contrast"
tools:text="2 active widgets" />
<TextView
android:id="@+id/activeWidgetsViewAction"
android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:gravity="center"
android:minWidth="48dp"
android:text="@string/active_widget_view_action"
android:textAllCaps="true"
android:textColor="?attr/colorAccent" />
</merge>

View file

@ -175,6 +175,11 @@
<color name="riotx_attachment_selector_border_dark">#FF22262E</color>
<color name="riotx_attachment_selector_border_black">#FF090A0C</color>
<attr name="riotx_room_active_widgets_banner" format="color" />
<color name="riotx_room_active_widgets_banner_light">#EBEFF5</color>
<color name="riotx_room_active_widgets_banner_dark">#27303A</color>
<color name="riotx_room_active_widgets_banner_black">#27303A</color>
<!-- (color from RiotWeb) -->
<attr name="riotx_keys_backup_banner_accent_color" format="color" />
<color name="riotx_keys_backup_banner_accent_color_light">#FFF8E3</color>

View file

@ -1116,6 +1116,7 @@
<item quantity="one">1 active widget</item>
<item quantity="other">%d active widgets</item>
</plurals>
<string name="active_widget_view_action">"VIEW"</string>
<string name="room_widget_activity_title">Widget</string>

View file

@ -33,6 +33,7 @@
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_black</item>
<item name="riotx_attachment_selector_background">@color/riotx_attachment_selector_background_black</item>
<item name="riotx_attachment_selector_border">@color/riotx_attachment_selector_border_black</item>
<item name="riotx_room_active_widgets_banner">@color/riotx_room_active_widgets_banner_black</item>
<!-- Drawables -->
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_black</item>

View file

@ -31,6 +31,7 @@
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_dark</item>
<item name="riotx_attachment_selector_background">@color/riotx_attachment_selector_background_dark</item>
<item name="riotx_attachment_selector_border">@color/riotx_attachment_selector_border_dark</item>
<item name="riotx_room_active_widgets_banner">@color/riotx_room_active_widgets_banner_dark</item>
<item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_dark</item>

View file

@ -32,6 +32,7 @@
<item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_light</item>
<item name="riotx_attachment_selector_background">@color/riotx_attachment_selector_background_light</item>
<item name="riotx_attachment_selector_border">@color/riotx_attachment_selector_border_light</item>
<item name="riotx_room_active_widgets_banner">@color/riotx_room_active_widgets_banner_light</item>
<!-- Drawables -->
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_light</item>