Merge branch 'develop' into feature/crypto

This commit is contained in:
ganfra 2019-06-07 19:25:55 +02:00
commit 07c516ccdd
62 changed files with 1275 additions and 587 deletions

View File

@ -48,7 +48,7 @@ android {
// TODO Set to false // TODO Set to false
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
// Set to BODY instead of NONE to enable logging // Set to BODY instead of NONE to enable logging
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.HEADERS" buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
} }


release { release {

View File

@ -15,7 +15,9 @@
*/ */
package im.vector.matrix.android.api.session.room.model.relation package im.vector.matrix.android.api.session.room.model.relation


import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable


/** /**
@ -91,4 +93,5 @@ interface RelationService {
*/ */
fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? fun replyToMessage(eventReplied: Event, replyText: String): Cancelable?


fun getEventSummaryLive(eventId: String): LiveData<List<EventAnnotationsSummary>>
} }

View File

@ -16,6 +16,7 @@


package im.vector.matrix.android.internal.database.mapper package im.vector.matrix.android.internal.database.mapper


import com.squareup.moshi.JsonDataException
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.UnsignedData import im.vector.matrix.android.api.session.events.model.UnsignedData
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
@ -46,8 +47,16 @@ internal object EventMapper {


fun map(eventEntity: EventEntity): Event { fun map(eventEntity: EventEntity): Event {
//TODO proxy the event to only parse unsigned data when accessed? //TODO proxy the event to only parse unsigned data when accessed?
var ud = if (eventEntity.unsignedData.isNullOrBlank()) null val ud = if (eventEntity.unsignedData.isNullOrBlank()) {
else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(eventEntity.unsignedData) null
} else {
try {
MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(eventEntity.unsignedData)
} catch (t: JsonDataException) {
null
}

}
return Event( return Event(
type = eventEntity.type, type = eventEntity.type,
eventId = eventEntity.eventId, eventId = eventEntity.eventId,

View File

@ -19,12 +19,10 @@ package im.vector.matrix.android.internal.database.model
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.Index import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import java.util.* import java.util.*
import kotlin.properties.Delegates


internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(), internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(),
@Index var eventId: String = "", @Index var eventId: String = "",
@ -51,10 +49,14 @@ internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUI


private var sendStateStr: String = SendState.UNKNOWN.name private var sendStateStr: String = SendState.UNKNOWN.name


@delegate:Ignore var sendState: SendState
var sendState: SendState by Delegates.observable(SendState.valueOf(sendStateStr)) { _, _, newValue -> get() {
sendStateStr = newValue.name return SendState.valueOf(sendStateStr)
} }
set(value) {
sendStateStr = value.name
}



companion object companion object



View File

@ -27,7 +27,7 @@ import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.create
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionAsync import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.Realm import io.realm.Realm
import timber.log.Timber import timber.log.Timber


@ -44,61 +44,79 @@ internal interface EventRelationsAggregationTask : Task<EventRelationsAggregatio
*/ */
internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarchy) : EventRelationsAggregationTask { internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarchy) : EventRelationsAggregationTask {


//OPT OUT serer aggregation until API mature enough
private val SHOULD_HANDLE_SERVER_AGREGGATION = false

override suspend fun execute(params: EventRelationsAggregationTask.Params): Try<Unit> { override suspend fun execute(params: EventRelationsAggregationTask.Params): Try<Unit> {
return monarchy.tryTransactionAsync { realm -> val events = params.events
update(realm, params.events, params.userId) val userId = params.userId
return monarchy.tryTransactionSync { realm ->
Timber.v(">>> DefaultEventRelationsAggregationTask[${params.hashCode()}] called with ${events.size} events")
update(realm, events, userId)
Timber.v("<<< DefaultEventRelationsAggregationTask[${params.hashCode()}] finished")
} }
} }


private fun update(realm: Realm, events: List<Pair<Event, SendState>>, userId: String) { private fun update(realm: Realm, events: List<Pair<Event, SendState>>, userId: String) {
events.forEach { pair -> events.forEach { pair ->
val roomId = pair.first.roomId ?: return@forEach try { //Temporary catch, should be removed
val event = pair.first val roomId = pair.first.roomId
val sendState = pair.second if (roomId == null) {
val isLocalEcho = sendState == SendState.UNSENT Timber.w("Event has no room id ${pair.first.eventId}")
when (event.type) { return@forEach
EventType.REACTION -> {
//we got a reaction!!
Timber.v("###REACTION in room $roomId")
handleReaction(event, roomId, realm, userId, isLocalEcho)
} }
EventType.MESSAGE -> { val event = pair.first
if (event.unsignedData?.relations?.annotations != null) { val sendState = pair.second
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") val isLocalEcho = sendState == SendState.UNSENT
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) when (event.type) {
} else { EventType.REACTION -> {
val content: MessageContent? = event.content.toModel() //we got a reaction!!
if (content?.relatesTo?.type == RelationType.REPLACE) { Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
Timber.v("###REPLACE in room $roomId for event ${event.eventId}") handleReaction(event, roomId, realm, userId, isLocalEcho)
//A replace!
handleReplace(realm, event, content, roomId, isLocalEcho)
}
} }

EventType.MESSAGE -> {
} if (event.unsignedData?.relations?.annotations != null) {
EventType.REDACTION -> { Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
?: return } else {
when (eventToPrune.type) { val content: MessageContent? = event.content.toModel()
EventType.MESSAGE -> { if (content?.relatesTo?.type == RelationType.REPLACE) {
Timber.d("REDACTION for message ${eventToPrune.eventId}") Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
val unsignedData = EventMapper.map(eventToPrune).unsignedData //A replace!
?: UnsignedData(null, null) handleReplace(realm, event, content, roomId, isLocalEcho)

//was this event a m.replace
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
} }

} }
EventType.REACTION -> {
handleReactionRedact(eventToPrune, realm, userId) }
EventType.REDACTION -> {
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
?: return@forEach
when (eventToPrune.type) {
EventType.MESSAGE -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}")
val unsignedData = EventMapper.map(eventToPrune).unsignedData
?: UnsignedData(null, null)

//was this event a m.replace
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
}

}
EventType.REACTION -> {
handleReactionRedact(eventToPrune, realm, userId)
}
} }
} }
else -> Timber.v("UnHandled event ${event.eventId}")
} }

} catch (t: Throwable) {
Timber.e(t, "## Should not happen ")
} }
} }

} }


private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean) { private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean) {
@ -108,7 +126,7 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc
//ok, this is a replace //ok, this is a replace
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
if (existing == null) { if (existing == null) {
Timber.v("###REPLACE creating no relation summary for ${targetEventId}") Timber.v("###REPLACE creating new relation summary for ${targetEventId}")
existing = EventAnnotationsSummaryEntity.create(realm, targetEventId) existing = EventAnnotationsSummaryEntity.create(realm, targetEventId)
existing.roomId = roomId existing.roomId = roomId
} }
@ -116,7 +134,7 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc
//we have it //we have it
val existingSummary = existing.editSummary val existingSummary = existing.editSummary
if (existingSummary == null) { if (existingSummary == null) {
Timber.v("###REPLACE no edit summary for ${targetEventId}, creating one (localEcho:$isLocalEcho)") Timber.v("###REPLACE new edit summary for ${targetEventId}, creating one (localEcho:$isLocalEcho)")
//create the edit summary //create the edit summary
val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java) val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java)
editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
@ -155,82 +173,92 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc
} }


private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) { private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) {
aggregation.chunk?.forEach { if (SHOULD_HANDLE_SERVER_AGREGGATION) {
if (it.type == EventType.REACTION) { aggregation.chunk?.forEach {
val eventId = event.eventId ?: "" if (it.type == EventType.REACTION) {
val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() val eventId = event.eventId ?: ""
if (existing == null) { val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId) if (existing == null) {
eventSummary.roomId = roomId val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId)
val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) eventSummary.roomId = roomId
sum.key = it.key val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order? sum.key = it.key
sum.count = it.count sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order?
eventSummary.reactionsSummary.add(sum) sum.count = it.count
} else { eventSummary.reactionsSummary.add(sum)
//TODO how to handle that } else {
//TODO how to handle that
}
} }
} }
} }
} }


private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) { private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) {
event.content.toModel<ReactionContent>()?.let { content -> val content = event.content.toModel<ReactionContent>()
//rel_type must be m.annotation if (content == null) {
if (RelationType.ANNOTATION == content.relatesTo?.type) { Timber.e("Malformed reaction content ${event.content}")
val reaction = content.relatesTo.key return
val eventId = content.relatesTo.eventId }
val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() //rel_type must be m.annotation
?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId } if (RelationType.ANNOTATION == content.relatesTo?.type) {
val reaction = content.relatesTo.key
val relatedEventID = content.relatesTo.eventId
val reactionEventId = event.eventId
Timber.v("Reaction $reactionEventId relates to $relatedEventID")
val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventID).findFirst()
?: EventAnnotationsSummaryEntity.create(realm, relatedEventID).apply { this.roomId = roomId }


var sum = eventSummary.reactionsSummary.find { it.key == reaction } var sum = eventSummary.reactionsSummary.find { it.key == reaction }
val txId = event.unsignedData?.transactionId val txId = event.unsignedData?.transactionId
if (isLocalEcho && txId.isNullOrBlank()) { if (isLocalEcho && txId.isNullOrBlank()) {
Timber.w("Received a local echo with no transaction ID") Timber.w("Received a local echo with no transaction ID")
} }
if (sum == null) { if (sum == null) {
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
sum.key = reaction sum.key = reaction
sum.firstTimestamp = event.originServerTs ?: 0 sum.firstTimestamp = event.originServerTs ?: 0
if (isLocalEcho) { if (isLocalEcho) {
Timber.v("Adding local echo reaction $reaction") Timber.v("Adding local echo reaction $reaction")
sum.sourceLocalEcho.add(txId) sum.sourceLocalEcho.add(txId)
sum.count = 1 sum.count = 1
} else {
Timber.v("Adding synced reaction $reaction")
sum.count = 1
sum.sourceEvents.add(event.eventId)
}
sum.addedByMe = sum.addedByMe || (userId == event.sender)
eventSummary.reactionsSummary.add(sum)
} else { } else {
//is this a known event (is possible? pagination?) Timber.v("Adding synced reaction $reaction")
if (!sum.sourceEvents.contains(eventId)) { sum.count = 1
sum.sourceEvents.add(reactionEventId)
}
sum.addedByMe = sum.addedByMe || (userId == event.sender)
eventSummary.reactionsSummary.add(sum)
} else {
//is this a known event (is possible? pagination?)
if (!sum.sourceEvents.contains(reactionEventId)) {


//check if it's not the sync of a local echo //check if it's not the sync of a local echo
if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) { if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) {
//ok it has already been counted, just sync the list, do not touch count //ok it has already been counted, just sync the list, do not touch count
Timber.v("Ignoring synced of local echo for reaction $reaction") Timber.v("Ignoring synced of local echo for reaction $reaction")
sum.sourceLocalEcho.remove(txId) sum.sourceLocalEcho.remove(txId)
sum.sourceEvents.add(event.eventId) sum.sourceEvents.add(reactionEventId)
} else {
sum.count += 1
if (isLocalEcho) {
Timber.v("Adding local echo reaction $reaction")
sum.sourceLocalEcho.add(txId)
} else { } else {
sum.count += 1 Timber.v("Adding synced reaction $reaction")
if (isLocalEcho) { sum.sourceEvents.add(reactionEventId)
Timber.v("Adding local echo reaction $reaction")
sum.sourceLocalEcho.add(txId)
} else {
Timber.v("Adding synced reaction $reaction")
sum.sourceEvents.add(event.eventId)
}

sum.addedByMe = sum.addedByMe || (userId == event.sender)
} }


sum.addedByMe = sum.addedByMe || (userId == event.sender)
} }
}


}
} }

} else {
Timber.e("Unknwon relation type ${content.relatesTo?.type} for event ${event.eventId}")
} }

} }


/** /**

View File

@ -46,11 +46,11 @@ internal class EventRelationsAggregationUpdater(monarchy: Monarchy,


override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) { override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
Timber.v("EventRelationsAggregationUpdater called with ${inserted.size} insertions") Timber.v("EventRelationsAggregationUpdater called with ${inserted.size} insertions")
val inserted = inserted val domainInserted = inserted
.mapNotNull { it.asDomain() to it.sendState } .map { it.asDomain() to it.sendState }


val params = EventRelationsAggregationTask.Params( val params = EventRelationsAggregationTask.Params(
inserted, domainInserted,
credentials.userId credentials.userId
) )



View File

@ -15,15 +15,19 @@
*/ */
package im.vector.matrix.android.internal.session.room.relation package im.vector.matrix.android.internal.session.room.relation


import androidx.lifecycle.LiveData
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.helper.addSendingEvent import im.vector.matrix.android.internal.database.helper.addSendingEvent
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
@ -169,6 +173,18 @@ internal class DefaultRelationService(private val roomId: String,
return CancelableWork(workRequest.id) return CancelableWork(workRequest.id)
} }



override fun getEventSummaryLive(eventId: String): LiveData<List<EventAnnotationsSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm ->
EventAnnotationsSummaryEntity.where(realm, eventId)
},
{
it.asDomain()
}
)
}

/** /**
* Saves the event in database as a local echo. * Saves the event in database as a local echo.
* SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room. * SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.

View File

@ -38,7 +38,12 @@ internal class TaskExecutor(private val coroutineDispatchers: MatrixCoroutineDis
task.execute(task.params) task.execute(task.params)
} }
} }
resultOrFailure.fold({ task.callback.onFailure(it) }, { task.callback.onSuccess(it) }) resultOrFailure.fold({
Timber.d(it, "Task failed")
task.callback.onFailure(it)
}, {
task.callback.onSuccess(it)
})
} }
return CancelableCoroutine(job) return CancelableCoroutine(job)
} }

View File

@ -84,6 +84,7 @@ android {
debug { debug {
resValue "bool", "debug_mode", "true" resValue "bool", "debug_mode", "true"
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
buildConfigField "boolean", "SHOW_HIDDEN_TIMELINE_EVENTS", "false"


signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
@ -91,6 +92,7 @@ android {
release { release {
resValue "bool", "debug_mode", "false" resValue "bool", "debug_mode", "false"
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
buildConfigField "boolean", "SHOW_HIDDEN_TIMELINE_EVENTS", "false"


minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

View File

@ -0,0 +1,22 @@
{
"data": [
{
"reaction" : "👍"
},
{
"reaction" : "😀"
},
{
"reaction" : "😞"
},
{
"reaction" : "Not a reaction"
},
{
"reaction" : "✅"
},
{
"reaction" : "🎉"
}
]
}

View File

@ -0,0 +1,48 @@
package im.vector.riotredesign

import android.graphics.Typeface
import androidx.core.provider.FontsContractCompat
import timber.log.Timber


class EmojiCompatFontProvider : FontsContractCompat.FontRequestCallback() {

var typeface: Typeface? = null
set(value) {
if (value != field) {
field = value
listeners.forEach {
try {
it.compatibilityFontUpdate(value)
} catch (t: Throwable) {
Timber.e(t)
}
}
}
}

private val listeners = ArrayList<FontProviderListener>()

override fun onTypefaceRetrieved(typeface: Typeface) {
this.typeface = typeface
}

override fun onTypefaceRequestFailed(reason: Int) {
Timber.e("Failed to load Emoji Compatible font, reason:$reason")
}

fun addListener(listener: FontProviderListener) {
if (!listeners.contains(listener)) {
listeners.add(listener)
}
}

fun removeListener(listener: FontProviderListener) {
listeners.remove(listener)
}


interface FontProviderListener {
fun compatibilityFontUpdate(typeface: Typeface?)
}
}

View File

@ -18,6 +18,10 @@ package im.vector.riotredesign


import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.HandlerThread
import androidx.core.provider.FontRequest
import androidx.core.provider.FontsContractCompat
import android.content.res.Configuration import android.content.res.Configuration
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyAsyncUtil
@ -43,6 +47,9 @@ import timber.log.Timber
class VectorApplication : Application() { class VectorApplication : Application() {


lateinit var appContext: Context lateinit var appContext: Context
//font thread handler
private var mFontThreadHandler: Handler? = null

val vectorConfiguration: VectorConfiguration by inject() val vectorConfiguration: VectorConfiguration by inject()


override fun onCreate() { override fun onCreate() {
@ -66,9 +73,20 @@ class VectorApplication : Application() {
val appModule = AppModule(applicationContext).definition val appModule = AppModule(applicationContext).definition
val homeModule = HomeModule().definition val homeModule = HomeModule().definition
val roomDirectoryModule = RoomDirectoryModule().definition val roomDirectoryModule = RoomDirectoryModule().definition
startKoin(listOf(appModule, homeModule, roomDirectoryModule), logger = EmptyLogger()) val koin = startKoin(listOf(appModule, homeModule, roomDirectoryModule), logger = EmptyLogger())
Matrix.getInstance().setApplicationFlavor(BuildConfig.FLAVOR_DESCRIPTION) Matrix.getInstance().setApplicationFlavor(BuildConfig.FLAVOR_DESCRIPTION)
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks()) registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks())

val fontRequest = FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Noto Color Emoji Compat",
R.array.com_google_android_gms_fonts_certs
)

// val efp = koin.koinContext.get<EmojiCompatFontProvider>()
FontsContractCompat.requestFont(this, fontRequest, koin.koinContext.get<EmojiCompatFontProvider>(), getFontThreadHandler())

vectorConfiguration.initConfiguration() vectorConfiguration.initConfiguration()
} }


@ -82,4 +100,13 @@ class VectorApplication : Application() {
vectorConfiguration.onConfigurationChanged(newConfig) vectorConfiguration.onConfigurationChanged(newConfig)
} }


private fun getFontThreadHandler(): Handler {
if (mFontThreadHandler == null) {
val handlerThread = HandlerThread("fonts")
handlerThread.start()
mFontThreadHandler = Handler(handlerThread.looper)
}
return mFontThreadHandler!!
}

} }

View File

@ -18,8 +18,8 @@ package im.vector.riotredesign.core.di


import android.content.Context import android.content.Context
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.EmojiCompatFontProvider
import im.vector.riotredesign.core.error.ErrorFormatter import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.resources.LocaleProvider import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringArrayProvider import im.vector.riotredesign.core.resources.StringArrayProvider
@ -91,8 +91,12 @@ class AppModule(private val context: Context) {
IncomingVerificationRequestHandler(context, get()) IncomingVerificationRequestHandler(context, get())
} }


factory { (fragment: Fragment) -> factory {
DefaultNavigator(fragment) as Navigator DefaultNavigator() as Navigator
}

single {
EmojiCompatFontProvider()
} }
} }
} }

View File

@ -28,4 +28,12 @@ object DimensionUtils {
context.resources.displayMetrics context.resources.displayMetrics
).toInt() ).toInt()
} }

fun spToPx(sp: Int, context: Context): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
sp.toFloat(),
context.resources.displayMetrics
).toInt()
}
} }

View File

@ -0,0 +1,69 @@
package im.vector.riotredesign.core.utils

import java.util.regex.Pattern

private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" +
"|[\uD83E\uDD00-\uD83E\uDDFF]" +
"|[\uD83D\uDE00-\uD83D\uDE4F]" +
"|[\uD83D\uDE80-\uD83D\uDEFF]" +
"|[\u2600-\u26FF]\uFE0F?" +
"|[\u2700-\u27BF]\uFE0F?" +
"|\u24C2\uFE0F?" +
"|[\uD83C\uDDE6-\uD83C\uDDFF]{1,2}" +
"|[\uD83C\uDD70\uD83C\uDD71\uD83C\uDD7E\uD83C\uDD7F\uD83C\uDD8E\uD83C\uDD91-\uD83C\uDD9A]\uFE0F?" +
"|[\u0023\u002A\u0030-\u0039]\uFE0F?\u20E3" +
"|[\u2194-\u2199\u21A9-\u21AA]\uFE0F?" +
"|[\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55]\uFE0F?" +
"|[\u2934\u2935]\uFE0F?" +
"|[\u3030\u303D]\uFE0F?" +
"|[\u3297\u3299]\uFE0F?" +
"|[\uD83C\uDE01\uD83C\uDE02\uD83C\uDE1A\uD83C\uDE2F\uD83C\uDE32-\uD83C\uDE3A\uD83C\uDE50\uD83C\uDE51]\uFE0F?" +
"|[\u203C\u2049]\uFE0F?" +
"|[\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE]\uFE0F?" +
"|[\u00A9\u00AE]\uFE0F?" +
"|[\u2122\u2139]\uFE0F?" +
"|\uD83C\uDC04\uFE0F?" +
"|\uD83C\uDCCF\uFE0F?" +
"|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))")

/**
* Test if a string contains emojis.
* It seems that the regex [emoji_regex]+ does not work.
* Some characters like ?, # or digit are accepted.
*
* @param str the body to test
* @return true if the body contains only emojis
*/
fun containsOnlyEmojis(str: String?): Boolean {
var res = false

if (str != null && str.isNotEmpty()) {
val matcher = emojisPattern.matcher(str)

var start = -1
var end = -1

while (matcher.find()) {
val nextStart = matcher.start()

// first emoji position
if (start < 0) {
if (nextStart > 0) {
return false
}
} else {
// must not have a character between
if (nextStart != end) {
return false
}
}
start = nextStart
end = matcher.end()
}

res = -1 != start && end == str.length
}

return res
}

View File

@ -0,0 +1,29 @@
package im.vector.riotredesign.core.utils

import java.util.*

object TextUtils {

private val suffixes = TreeMap<Int, String>().also {
it.put(1000, "k")
it.put(1000000, "M")
it.put(1000000000, "G")
}

fun formatCountToShortDecimal(value: Int): String {
try {
if (value < 0) return "-" + formatCountToShortDecimal(-value)
if (value < 1000) return value.toString() //deal with easy case

val e = suffixes.floorEntry(value)
val divideBy = e.key
val suffix = e.value

val truncated = value / (divideBy!! / 10) //the number part of the output times 10
val hasDecimal = truncated < 100 && truncated / 10.0 != (truncated / 10).toDouble()
return if (hasDecimal) "${truncated / 10.0}$suffix" else "${truncated / 10}$suffix"
} catch (t: Throwable) {
return value.toString()
}
}
}

View File

@ -54,12 +54,12 @@ class HomeDrawerFragment : VectorBaseFragment() {
} }
} }
homeDrawerHeaderSettingsView.setOnClickListener { homeDrawerHeaderSettingsView.setOnClickListener {
navigator.openSettings() navigator.openSettings(requireActivity())
} }


// Debug menu // Debug menu
homeDrawerHeaderDebugView.setOnClickListener { homeDrawerHeaderDebugView.setOnClickListener {
navigator.openDebug() navigator.openDebug(requireActivity())
} }
} }
} }

View File

@ -69,7 +69,9 @@ class HomeModule {
val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get()) val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get())
val noticeEventFormatter = get<NoticeEventFormatter>(parameters = { parametersOf(fragment) }) val noticeEventFormatter = get<NoticeEventFormatter>(parameters = { parametersOf(fragment) })
val timelineMediaSizeProvider = TimelineMediaSizeProvider() val timelineMediaSizeProvider = TimelineMediaSizeProvider()
val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer, get()) val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider,
timelineDateFormatter, eventHtmlRenderer, get(), get())

val timelineItemFactory = TimelineItemFactory( val timelineItemFactory = TimelineItemFactory(
messageItemFactory = messageItemFactory, messageItemFactory = messageItemFactory,
noticeItemFactory = NoticeItemFactory(noticeEventFormatter), noticeItemFactory = NoticeItemFactory(noticeEventFormatter),

View File

@ -48,7 +48,7 @@ class HomeNavigator {
activity?.let { activity?.let {
//TODO enable eventId permalink. It doesn't work enough at the moment. //TODO enable eventId permalink. It doesn't work enough at the moment.
it.drawerLayout?.closeDrawer(GravityCompat.START) it.drawerLayout?.closeDrawer(GravityCompat.START)
navigator.openRoom(roomId) navigator.openRoom(roomId, it)
} }
} }



View File

@ -53,6 +53,7 @@ import com.jaiselrahman.filepicker.model.MediaFile
import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
@ -84,6 +85,7 @@ import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventCo
import im.vector.riotredesign.features.home.room.detail.timeline.action.ActionsHandler import im.vector.riotredesign.features.home.room.detail.timeline.action.ActionsHandler
import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageMenuViewModel import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageMenuViewModel
import im.vector.riotredesign.features.home.room.detail.timeline.action.ViewReactionBottomSheet
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotredesign.features.html.PillImageSpan import im.vector.riotredesign.features.html.PillImageSpan
@ -235,11 +237,13 @@ class RoomDetailFragment :
var formattedBody: CharSequence? = null var formattedBody: CharSequence? = null
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build() val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody ?: messageContent.body) val document = parser.parse(messageContent.formattedBody
?: messageContent.body)
formattedBody = Markwon.builder(requireContext()) formattedBody = Markwon.builder(requireContext())
.usePlugin(HtmlPlugin.create()).build().render(document) .usePlugin(HtmlPlugin.create()).build().render(document)
} }
composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody composerLayout.composerRelatedMessageContent.text = formattedBody
?: nonFormattedBody




if (mode == SendMode.EDIT) { if (mode == SendMode.EDIT) {
@ -559,11 +563,11 @@ class RoomDetailFragment :
vectorBaseActivity.notImplemented() vectorBaseActivity.notImplemented()
} }


override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View) { override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) {


} }


override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean { override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val roomId = roomDetailArgs.roomId val roomId = roomDetailArgs.roomId


@ -593,6 +597,11 @@ class RoomDetailFragment :
} }
} }


override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) {
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
}

override fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) { override fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) {
editAggregatedSummary?.also { editAggregatedSummary?.also {
roomDetailViewModel.process(RoomDetailActions.ShowEditHistoryAction(informationData.eventId, it)) roomDetailViewModel.process(RoomDetailActions.ShowEditHistoryAction(informationData.eventId, it))
@ -613,12 +622,17 @@ class RoomDetailFragment :
val eventId = actionData.data?.toString() ?: return val eventId = actionData.data?.toString() ?: return
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), eventId), REACTION_SELECT_REQUEST_CODE) startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), eventId), REACTION_SELECT_REQUEST_CODE)
} }
MessageMenuViewModel.ACTION_VIEW_REACTIONS -> {
val messageInformationData = actionData.data as? MessageInformationData
?: return
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
}
MessageMenuViewModel.ACTION_COPY -> { MessageMenuViewModel.ACTION_COPY -> {
//I need info about the current selected message :/ //I need info about the current selected message :/
copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false) copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false)
val snack = Snackbar.make(view!!, requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) val msg = requireContext().getString(R.string.copied_to_clipboard)
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color)) showSnackWithMessage(msg, Snackbar.LENGTH_SHORT)
snack.show()
} }
MessageMenuViewModel.ACTION_DELETE -> { MessageMenuViewModel.ACTION_DELETE -> {
val eventId = actionData.data?.toString() ?: return val eventId = actionData.data?.toString() ?: return
@ -685,6 +699,13 @@ class RoomDetailFragment :
val eventId = actionData.data.toString() val eventId = actionData.data.toString()
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
} }
MessageMenuViewModel.ACTION_COPY_PERMALINK -> {
val eventId = actionData.data.toString()
val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, eventId)
copyToClipboard(requireContext(), permalink, false)
showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)

}
else -> { else -> {
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show() Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show()
} }

View File

@ -55,7 +55,12 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
private val roomId = initialState.roomId private val roomId = initialState.roomId
private val eventId = initialState.eventId private val eventId = initialState.eventId
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>() private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES) private val allowedTypes = if (TimelineDisplayableEvents.DEBUG_HIDDEN_EVENT) {
TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES
} else {
TimelineDisplayableEvents.DISPLAYABLE_TYPES
}
private val timeline = room.createTimeline(eventId, allowedTypes)


companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> { companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {


@ -195,7 +200,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
} }
} }
SendMode.EDIT -> { SendMode.EDIT -> {
room.editTextMessage(state.selectedEvent?.root?.eventId ?: "", action.text, action.autoMarkdown) room.editTextMessage(state.selectedEvent?.root?.eventId
?: "", action.text, action.autoMarkdown)
setState { setState {
copy( copy(
sendMode = SendMode.REGULAR, sendMode = SendMode.REGULAR,
@ -330,7 +336,6 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
room.updateQuickReaction(action.selectedReaction, action.opposite, action.targetEventId, session.sessionParams.credentials.userId) room.updateQuickReaction(action.selectedReaction, action.opposite, action.targetEventId, session.sessionParams.credentials.userId)
} }



private fun handleSendMedia(action: RoomDetailActions.SendMedia) { private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
val attachments = action.mediaFiles.map { val attachments = action.mediaFiles.map {
ContentAttachmentData( ContentAttachmentData(
@ -350,6 +355,12 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,


private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
displayedEventsObservable.accept(action) displayedEventsObservable.accept(action)
//We need to update this with the related m.replace also (to move read receipt)
action.event.annotations?.editSummary?.sourceEvents?.forEach {
room.getTimeLineEvent(it)?.let { event ->
displayedEventsObservable.accept(RoomDetailActions.EventDisplayed(event))
}
}
} }


private fun handleLoadMore(action: RoomDetailActions.LoadMore) { private fun handleLoadMore(action: RoomDetailActions.LoadMore) {

View File

@ -46,22 +46,29 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler() private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler()
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {


interface Callback : ReactionPillCallback { interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback {
fun onEventVisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent)
fun onUrlClicked(url: String) fun onUrlClicked(url: String)
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
fun onFileMessageClicked(messageFileContent: MessageFileContent) fun onFileMessageClicked(messageFileContent: MessageFileContent)
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View)
fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean
fun onAvatarClicked(informationData: MessageInformationData)
fun onMemberNameClicked(informationData: MessageInformationData)
fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?)
} }


interface ReactionPillCallback { interface ReactionPillCallback {
fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean)
fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String)
}

interface BaseCallback {
fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View)
fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean
}

interface AvatarCallback {
fun onAvatarClicked(informationData: MessageInformationData)
fun onMemberNameClicked(informationData: MessageInformationData)
} }


private val collapsedEventIds = linkedSetOf<String>() private val collapsedEventIds = linkedSetOf<String>()

View File

@ -17,13 +17,13 @@ package im.vector.riotredesign.features.home.room.detail.timeline.action


import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -33,10 +33,9 @@ import com.airbnb.mvrx.withState
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.bottom_sheet_message_actions.*


/** /**
* Bottom sheet fragment that shows a message preview with list of contextual actions * Bottom sheet fragment that shows a message preview with list of contextual actions
@ -74,7 +73,7 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
val cfm = childFragmentManager val cfm = childFragmentManager
var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment
if (menuActionFragment == null) { if (menuActionFragment == null) {
menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as ParcelableArgs) menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
cfm.beginTransaction() cfm.beginTransaction()
.replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment") .replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment")
.commit() .commit()
@ -89,7 +88,7 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {


var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment
if (quickReactionFragment == null) { if (quickReactionFragment == null) {
quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as ParcelableArgs) quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
cfm.beginTransaction() cfm.beginTransaction()
.replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction") .replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction")
.commit() .commit()
@ -117,36 +116,26 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
} }


override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
senderNameTextView.text = it.senderName if (it.showPreview) {
messageBodyTextView.text = it.messageBody bottom_sheet_message_preview.isVisible = true
messageTimestampText.text = it.ts senderNameTextView.text = it.senderName

messageBodyTextView.text = it.messageBody
GlideApp.with(this).clear(senderAvatarImageView) messageTimestampText.text = it.ts
if (it.senderAvatarPath != null) { AvatarRenderer.render(it.senderAvatarPath, it.userId, it.senderName, senderAvatarImageView)
GlideApp.with(this)
.load(it.senderAvatarPath)
.circleCrop()
.placeholder(AvatarRenderer.getPlaceholderDrawable(requireContext(), it.userId, it.senderName))
.into(senderAvatarImageView)
} else { } else {
senderAvatarImageView.setImageDrawable(AvatarRenderer.getPlaceholderDrawable(requireContext(), it.userId, it.senderName)) bottom_sheet_message_preview.isVisible = false
} }
quickReactBottomDivider.isVisible = it.canReact
bottom_sheet_quick_reaction_container.isVisible = it.canReact
return@withState return@withState
} }




@Parcelize
data class ParcelableArgs(
val eventId: String,
val roomId: String,
val informationData: MessageInformationData
) : Parcelable

companion object { companion object {
fun newInstance(roomId: String, informationData: MessageInformationData): MessageActionsBottomSheet { fun newInstance(roomId: String, informationData: MessageInformationData): MessageActionsBottomSheet {
return MessageActionsBottomSheet().apply { return MessageActionsBottomSheet().apply {
setArguments( setArguments(
ParcelableArgs( TimelineEventFragmentArgs(
informationData.eventId, informationData.eventId,
roomId, roomId,
informationData informationData

View File

@ -15,18 +15,21 @@
*/ */
package im.vector.riotredesign.features.home.room.detail.timeline.action package im.vector.riotredesign.features.home.room.detail.timeline.action


import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.session.Session 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.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.riotredesign.core.platform.VectorViewModel import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.core.parameter.parametersOf
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import ru.noties.markwon.html.HtmlPlugin import ru.noties.markwon.html.HtmlPlugin
import timber.log.Timber import timber.log.Timber
@ -35,10 +38,12 @@ import java.util.*




data class MessageActionState( data class MessageActionState(
val userId: String, val userId: String = "",
val senderName: String, val senderName: String = "",
val messageBody: CharSequence, val messageBody: CharSequence? = null,
val ts: String?, val ts: String? = null,
val showPreview: Boolean = false,
val canReact: Boolean = false,
val senderAvatarPath: String? = null) val senderAvatarPath: String? = null)
: MvRxState : MvRxState


@ -51,30 +56,47 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode


override fun initialState(viewModelContext: ViewModelContext): MessageActionState? { override fun initialState(viewModelContext: ViewModelContext): MessageActionState? {
val currentSession = viewModelContext.activity.get<Session>() val currentSession = viewModelContext.activity.get<Session>()
val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs val fragment = (viewModelContext as? FragmentViewModelContext)?.fragment
val noticeFormatter = fragment?.get<NoticeEventFormatter>(parameters = { parametersOf(fragment) })
val parcel = viewModelContext.args as TimelineEventFragmentArgs


val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault()) val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())


val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId) val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
var body: CharSequence? = null
val originTs = event?.root?.originServerTs
return if (event != null) { return if (event != null) {
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() when (event.root.type) {
?: event.root.content.toModel() EventType.MESSAGE -> {
val originTs = event.root.originServerTs val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel()
var body: CharSequence = messageContent?.body ?: "" ?: event.root.content.toModel()
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { body = messageContent?.body
val parser = Parser.builder().build() if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val document = parser.parse(messageContent.formattedBody?.trim() ?: messageContent.body) val parser = Parser.builder().build()
// val renderer = HtmlRenderer.builder().build() val document = parser.parse(messageContent.formattedBody
body = Markwon.builder(viewModelContext.activity) ?: messageContent.body)
.usePlugin(HtmlPlugin.create()).build().render(document) body = Markwon.builder(viewModelContext.activity)
// body = renderer.render(document) .usePlugin(HtmlPlugin.create()).build().render(document)
}
}
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_HISTORY_VISIBILITY,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> {
body = noticeFormatter?.format(event)
}
} }
MessageActionState( MessageActionState(
event.root.sender ?: "", userId = event.root.sender ?: "",
parcel.informationData.memberName.toString(), senderName = parcel.informationData.memberName?.toString() ?: "",
body, messageBody = body,
dateFormat.format(Date(originTs ?: 0)), ts = dateFormat.format(Date(originTs ?: 0)),
currentSession.contentUrlResolver().resolveFullSize(parcel.informationData.avatarUrl) showPreview = body != null,
canReact = event.root.type == EventType.MESSAGE && event.sendState.isSent(),
senderAvatarPath = parcel.informationData.avatarUrl
) )
} else { } else {
//can this happen? //can this happen?

View File

@ -101,7 +101,7 @@ class MessageMenuFragment : BaseMvRxFragment() {




companion object { companion object {
fun newInstance(pa: MessageActionsBottomSheet.ParcelableArgs): MessageMenuFragment { fun newInstance(pa: TimelineEventFragmentArgs): MessageMenuFragment {
val args = Bundle() val args = Bundle()
args.putParcelable(MvRx.KEY_ARG, pa) args.putParcelable(MvRx.KEY_ARG, pa)
val fragment = MessageMenuFragment() val fragment = MessageMenuFragment()

View File

@ -34,7 +34,7 @@ import org.koin.android.ext.android.get


data class SimpleAction(val uid: String, val titleRes: Int, val iconResId: Int?, val data: Any? = null) data class SimpleAction(val uid: String, val titleRes: Int, val iconResId: Int?, val data: Any? = null)


data class MessageMenuState(val actions: List<SimpleAction>) : MvRxState data class MessageMenuState(val actions: List<SimpleAction> = emptyList()) : MvRxState


/** /**
* Manages list actions for a given message (copy / paste / forward...) * Manages list actions for a given message (copy / paste / forward...)
@ -46,27 +46,26 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
override fun initialState(viewModelContext: ViewModelContext): MessageMenuState? { override fun initialState(viewModelContext: ViewModelContext): MessageMenuState? {
// Args are accessible from the context. // Args are accessible from the context.
val currentSession = viewModelContext.activity.get<Session>() val currentSession = viewModelContext.activity.get<Session>()
val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs val parcel = viewModelContext.args as TimelineEventFragmentArgs
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId) val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
?: return null ?: return null


val messageContent: MessageContent = event.annotations?.editSummary?.aggregatedContent?.toModel() val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.content.toModel() ?: return null ?: event.root.content.toModel()
val type = messageContent.type val type = messageContent?.type


if (event.sendState == SendState.UNSENT) { if (!event.sendState.isSent()) {
//Resend and Delete //Resend and Delete
return MessageMenuState( return MessageMenuState(
//TODO
listOf( listOf(
SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_send, event.root.eventId), // SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_send, event.root.eventId),
//TODO delete icon // //TODO delete icon
SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId) // SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId)
) )
) )
} }



//TODO determine if can copy, forward, reply, quote, report?
val actions = ArrayList<SimpleAction>().apply { val actions = ArrayList<SimpleAction>().apply {


if (event.sendState == SendState.SENDING) { if (event.sendState == SendState.SENDING) {
@ -75,10 +74,12 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
} }
//TODO is downloading attachement? //TODO is downloading attachement?


this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, event.root.eventId)) if (canReact(event, messageContent)) {
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, event.root.eventId))
}
if (canCopy(type)) { if (canCopy(type)) {
//TODO copy images? html? see ClipBoard //TODO copy images? html? see ClipBoard
this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent.body)) this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent!!.body))
} }


if (canReply(event, messageContent)) { if (canReply(event, messageContent)) {
@ -94,10 +95,13 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
} }


if (canQuote(event, messageContent)) { if (canQuote(event, messageContent)) {
//TODO quote icon
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, parcel.eventId)) this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, parcel.eventId))
} }


if (canViewReactions(event)) {
this.add(SimpleAction(ACTION_VIEW_REACTIONS, R.string.message_view_reaction, R.drawable.ic_view_reactions, parcel.informationData))
}

if (canShare(type)) { if (canShare(type)) {
if (messageContent is MessageImageContent) { if (messageContent is MessageImageContent) {
this.add( this.add(
@ -117,14 +121,13 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
//TODO sent by me or sufficient power level //TODO sent by me or sufficient power level
} }



this.add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, JSONObject(event.root.toContent()).toString(4))) this.add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, JSONObject(event.root.toContent()).toString(4)))
if (event.isEncrypted()) { if (event.isEncrypted()) {
this.add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, parcel.eventId)) this.add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, parcel.eventId))
} }
this.add(SimpleAction(PERMALINK, R.string.permalink, R.drawable.ic_permalink, parcel.eventId)) this.add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, parcel.eventId))


if (currentSession.sessionParams.credentials.userId != event.root.sender) { if (currentSession.sessionParams.credentials.userId != event.root.sender && event.root.getClearType() == EventType.MESSAGE) {
//not sent by me //not sent by me
this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, parcel.eventId)) this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, parcel.eventId))
} }
@ -133,10 +136,10 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
return MessageMenuState(actions) return MessageMenuState(actions)
} }


private fun canReply(event: TimelineEvent, messageContent: MessageContent): Boolean { private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false if (event.root.getClearType() != EventType.MESSAGE) return false
return when (messageContent.type) { return when (messageContent?.type) {
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_EMOTE,
@ -148,10 +151,15 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
} }
} }


private fun canQuote(event: TimelineEvent, messageContent: MessageContent): Boolean { private fun canReact(event: TimelineEvent, messageContent: MessageContent?): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
return event.root.getClearType() == EventType.MESSAGE
}

private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false if (event.root.getClearType() != EventType.MESSAGE) return false
return when (messageContent.type) { return when (messageContent?.type) {
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_EMOTE,
@ -165,14 +173,21 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes


private fun canRedact(event: TimelineEvent, myUserId: String): Boolean { private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.type != EventType.MESSAGE) return false if (event.root.getClearType() != EventType.MESSAGE) return false
//TODO if user is admin or moderator //TODO if user is admin or moderator
return event.root.sender == myUserId return event.root.sender == myUserId
} }


private fun canViewReactions(event: TimelineEvent): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
//TODO if user is admin or moderator
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
}

private fun canEdit(event: TimelineEvent, myUserId: String): Boolean { private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.type != EventType.MESSAGE) return false if (event.root.getClearType() != EventType.MESSAGE) return false
//TODO if user is admin or moderator //TODO if user is admin or moderator
val messageContent = event.root.content.toModel<MessageContent>() val messageContent = event.root.content.toModel<MessageContent>()
return event.root.sender == myUserId && ( return event.root.sender == myUserId && (
@ -182,7 +197,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
} }




private fun canCopy(type: String): Boolean { private fun canCopy(type: String?): Boolean {
return when (type) { return when (type) {
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_NOTICE,
@ -196,7 +211,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
} }




private fun canShare(type: String): Boolean { private fun canShare(type: String?): Boolean {
return when (type) { return when (type) {
MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_AUDIO,
@ -217,9 +232,10 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
const val ACTION_DELETE = "delete" const val ACTION_DELETE = "delete"
const val VIEW_SOURCE = "VIEW_SOURCE" const val VIEW_SOURCE = "VIEW_SOURCE"
const val VIEW_DECRYPTED_SOURCE = "VIEW_DECRYPTED_SOURCE" const val VIEW_DECRYPTED_SOURCE = "VIEW_DECRYPTED_SOURCE"
const val PERMALINK = "PERMALINK" const val ACTION_COPY_PERMALINK = "ACTION_COPY_PERMALINK"
const val ACTION_FLAG = "ACTION_FLAG" const val ACTION_FLAG = "ACTION_FLAG"
const val ACTION_QUICK_REACT = "ACTION_QUICK_REACT" const val ACTION_QUICK_REACT = "ACTION_QUICK_REACT"
const val ACTION_VIEW_REACTIONS = "ACTION_VIEW_REACTIONS"




} }

View File

@ -15,6 +15,7 @@
*/ */
package im.vector.riotredesign.features.home.room.detail.timeline.action package im.vector.riotredesign.features.home.room.detail.timeline.action


import android.graphics.Typeface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -28,7 +29,9 @@ import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotredesign.EmojiCompatFontProvider
import im.vector.riotredesign.R import im.vector.riotredesign.R
import org.koin.android.ext.android.inject


/** /**
* Quick Reaction Fragment (agree / like reactions) * Quick Reaction Fragment (agree / like reactions)
@ -54,6 +57,8 @@ class QuickReactionFragment : BaseMvRxFragment() {


var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null


val fontProvider by inject<EmojiCompatFontProvider>()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.adapter_item_action_quick_reaction, container, false) val view = inflater.inflate(R.layout.adapter_item_action_quick_reaction, container, false)
ButterKnife.bind(this, view) ButterKnife.bind(this, view)
@ -68,6 +73,10 @@ class QuickReactionFragment : BaseMvRxFragment() {
quickReact3Text.text = QuickReactionViewModel.likePositive quickReact3Text.text = QuickReactionViewModel.likePositive
quickReact4Text.text = QuickReactionViewModel.likeNegative quickReact4Text.text = QuickReactionViewModel.likeNegative


listOf(quickReact1Text, quickReact2Text, quickReact3Text, quickReact4Text).forEach {
it.typeface = fontProvider.typeface ?: Typeface.DEFAULT
}

//configure click listeners //configure click listeners
quickReact1Text.setOnClickListener { quickReact1Text.setOnClickListener {
viewModel.toggleAgree(true) viewModel.toggleAgree(true)
@ -88,11 +97,11 @@ class QuickReactionFragment : BaseMvRxFragment() {


TransitionManager.beginDelayedTransition(rootLayout) TransitionManager.beginDelayedTransition(rootLayout)
when (it.agreeTrigleState) { when (it.agreeTrigleState) {
TriggleState.NONE -> { TriggleState.NONE -> {
quickReact1Text.alpha = 1f quickReact1Text.alpha = 1f
quickReact2Text.alpha = 1f quickReact2Text.alpha = 1f
} }
TriggleState.FIRST -> { TriggleState.FIRST -> {
quickReact1Text.alpha = 1f quickReact1Text.alpha = 1f
quickReact2Text.alpha = 0.2f quickReact2Text.alpha = 0.2f


@ -103,11 +112,11 @@ class QuickReactionFragment : BaseMvRxFragment() {
} }
} }
when (it.likeTriggleState) { when (it.likeTriggleState) {
TriggleState.NONE -> { TriggleState.NONE -> {
quickReact3Text.alpha = 1f quickReact3Text.alpha = 1f
quickReact4Text.alpha = 1f quickReact4Text.alpha = 1f
} }
TriggleState.FIRST -> { TriggleState.FIRST -> {
quickReact3Text.alpha = 1f quickReact3Text.alpha = 1f
quickReact4Text.alpha = 0.2f quickReact4Text.alpha = 0.2f


@ -130,7 +139,7 @@ class QuickReactionFragment : BaseMvRxFragment() {
} }


companion object { companion object {
fun newInstance(pa: MessageActionsBottomSheet.ParcelableArgs): QuickReactionFragment { fun newInstance(pa: TimelineEventFragmentArgs): QuickReactionFragment {
val args = Bundle() val args = Bundle()
args.putParcelable(MvRx.KEY_ARG, pa) args.putParcelable(MvRx.KEY_ARG, pa)
val fragment = QuickReactionFragment() val fragment = QuickReactionFragment()

View File

@ -32,15 +32,14 @@ enum class TriggleState {
} }


data class QuickReactionState( data class QuickReactionState(
val agreeTrigleState: TriggleState, val agreeTrigleState: TriggleState = TriggleState.NONE,
val likeTriggleState: TriggleState, val likeTriggleState: TriggleState = TriggleState.NONE,
/** Pair of 'clickedOn' and current toggles state*/ /** Pair of 'clickedOn' and current toggles state*/
val selectionResult: Pair<String, List<String>>? = null, val selectionResult: Pair<String, List<String>>? = null,
val eventId: String) : MvRxState val eventId: String = "") : MvRxState


/** /**
* Quick reaction view model * Quick reaction view model
* TODO: configure initial state from event
*/ */
class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel<QuickReactionState>(initialState) { class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel<QuickReactionState>(initialState) {


@ -88,15 +87,15 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
private fun getReactions(state: QuickReactionState, newState1: TriggleState?, newState2: TriggleState?): List<String> { private fun getReactions(state: QuickReactionState, newState1: TriggleState?, newState2: TriggleState?): List<String> {
return ArrayList<String>(4).apply { return ArrayList<String>(4).apply {
when (newState2 ?: state.likeTriggleState) { when (newState2 ?: state.likeTriggleState) {
TriggleState.FIRST -> add(likePositive) TriggleState.FIRST -> add(likePositive)
TriggleState.SECOND -> add(likeNegative) TriggleState.SECOND -> add(likeNegative)
else -> { else -> {
} }
} }
when (newState1 ?: state.agreeTrigleState) { when (newState1 ?: state.agreeTrigleState) {
TriggleState.FIRST -> add(agreePositive) TriggleState.FIRST -> add(agreePositive)
TriggleState.SECOND -> add(agreeNegative) TriggleState.SECOND -> add(agreeNegative)
else -> { else -> {
} }
} }
} }
@ -114,9 +113,9 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
return when (reaction) { return when (reaction) {
agreePositive -> agreeNegative agreePositive -> agreeNegative
agreeNegative -> agreePositive agreeNegative -> agreePositive
likePositive -> likeNegative likePositive -> likeNegative
likeNegative -> likePositive likeNegative -> likePositive
else -> null else -> null
} }
} }


@ -124,7 +123,7 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
// Args are accessible from the context. // Args are accessible from the context.
// val foo = vieWModelContext.args<MyArgs>.foo // val foo = vieWModelContext.args<MyArgs>.foo
val currentSession = viewModelContext.activity.get<Session>() val currentSession = viewModelContext.activity.get<Session>()
val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs val parcel = viewModelContext.args as TimelineEventFragmentArgs
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId) val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
?: return null ?: return null
var agreeTriggle: TriggleState = TriggleState.NONE var agreeTriggle: TriggleState = TriggleState.NONE

View File

@ -0,0 +1,46 @@
package im.vector.riotredesign.features.home.room.detail.timeline.action

import android.graphics.Typeface
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder

/**
* Item displaying an emoji reaction (single line with emoji, author, time)
*/
@EpoxyModelClass(layout = R.layout.item_simple_reaction_info)
abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder<ReactionInfoSimpleItem.Holder>() {

@EpoxyAttribute
lateinit var reactionKey: CharSequence
@EpoxyAttribute
lateinit var authorDisplayName: CharSequence
@EpoxyAttribute
var timeStamp: CharSequence? = null

@EpoxyAttribute
var emojiTypeFace: Typeface? = null

override fun bind(holder: Holder) {
holder.emojiReactionView.text = reactionKey
holder.emojiReactionView.typeface = emojiTypeFace ?: Typeface.DEFAULT
holder.displayNameView.text = authorDisplayName
timeStamp?.let {
holder.timeStampView.text = it
holder.timeStampView.isVisible = true
} ?: run {
holder.timeStampView.isVisible = false
}
}

class Holder : VectorEpoxyHolder() {
val emojiReactionView by bind<TextView>(R.id.itemSimpleReactionInfoKey)
val displayNameView by bind<TextView>(R.id.itemSimpleReactionInfoMemberName)
val timeStampView by bind<TextView>(R.id.itemSimpleReactionInfoTime)
}

}

View File

@ -0,0 +1,12 @@
package im.vector.riotredesign.features.home.room.detail.timeline.action

import android.os.Parcelable
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.android.parcel.Parcelize

@Parcelize
data class TimelineEventFragmentArgs(
val eventId: String,
val roomId: String,
val informationData: MessageInformationData
) : Parcelable

View File

@ -0,0 +1,74 @@
package im.vector.riotredesign.features.home.room.detail.timeline.action

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import butterknife.BindView
import butterknife.ButterKnife
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotredesign.EmojiCompatFontProvider
import im.vector.riotredesign.R
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.android.synthetic.main.bottom_sheet_display_reactions.*
import org.koin.android.ext.android.inject

/**
* Bottom sheet displaying list of reactions for a given event ordered by timestamp
*/
class ViewReactionBottomSheet : BaseMvRxBottomSheetDialog() {

private val viewModel: ViewReactionViewModel by fragmentViewModel(ViewReactionViewModel::class)

private val emojiCompatFontProvider by inject<EmojiCompatFontProvider>()

@BindView(R.id.bottom_sheet_display_reactions_list)
lateinit var epoxyRecyclerView: EpoxyRecyclerView

private val epoxyController by lazy { ViewReactionsEpoxyController(emojiCompatFontProvider.typeface) }

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_display_reactions, container, false)
ButterKnife.bind(this, view)
return view
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
epoxyRecyclerView.setController(epoxyController)
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
}


override fun invalidate() = withState(viewModel) {
if (it.mapReactionKeyToMemberList() == null) {
bottomSheetViewReactionSpinner.isVisible = true
bottomSheetViewReactionSpinner.animate()
} else {
bottomSheetViewReactionSpinner.isVisible = false
}
epoxyController.setData(it)
}

companion object {
fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionBottomSheet {
val args = Bundle()
val parcelableArgs = TimelineEventFragmentArgs(
informationData.eventId,
roomId,
informationData
)
args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
return ViewReactionBottomSheet().apply { arguments = args }

}
}
}

View File

@ -0,0 +1,101 @@
package im.vector.riotredesign.features.home.room.detail.timeline.action

import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
import org.koin.android.ext.android.get


data class DisplayReactionsViewState(
val eventId: String = "",
val roomId: String = "",
val mapReactionKeyToMemberList: Async<List<ReactionInfo>> = Uninitialized)
: MvRxState

data class ReactionInfo(
val eventId: String,
val reactionKey: String,
val authorId: String,
val authorName: String? = null,
val timestamp: String? = null
)

/**
* Used to display the list of members that reacted to a given event
*/
class ViewReactionViewModel(private val session: Session,
private val timelineDateFormatter: TimelineDateFormatter,
initialState: DisplayReactionsViewState) : VectorViewModel<DisplayReactionsViewState>(initialState) {

init {
loadReaction()
}

fun loadReaction() = withState { state ->

try {
val room = session.getRoom(state.roomId)
val event = room?.getTimeLineEvent(state.eventId)
if (event == null) {
setState { copy(mapReactionKeyToMemberList = Fail(Throwable())) }
return@withState
}
var results = ArrayList<ReactionInfo>()
event.annotations?.reactionsSummary?.forEach { sum ->

sum.sourceEvents.mapNotNull { room.getTimeLineEvent(it) }.forEach {
val localDate = it.root.localDateTime()
results.add(ReactionInfo(it.root.eventId!!, sum.key, it.root.sender
?: "", it.senderName, timelineDateFormatter.formatMessageHour(localDate)))
}
}
setState {
copy(
mapReactionKeyToMemberList = Success(results.sortedBy { it.timestamp })
)
}
} catch (t: Throwable) {
setState {
copy(
mapReactionKeyToMemberList = Fail(t)
)
}
}
}


companion object : MvRxViewModelFactory<ViewReactionViewModel, DisplayReactionsViewState> {

override fun initialState(viewModelContext: ViewModelContext): DisplayReactionsViewState? {

val roomId = (viewModelContext.args as? TimelineEventFragmentArgs)?.roomId
?: return null
val info = (viewModelContext.args as? TimelineEventFragmentArgs)?.informationData
?: return null
return DisplayReactionsViewState(info.eventId, roomId)
}

override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionViewModel? {
val session = viewModelContext.activity.get<Session>()
val eventId = (viewModelContext.args as TimelineEventFragmentArgs).eventId
val lifecycleOwner = (viewModelContext as FragmentViewModelContext).fragment<Fragment>()
val liveSummary = session.getRoom(state.roomId)?.getEventSummaryLive(eventId)
val viewReactionViewModel = ViewReactionViewModel(session, viewModelContext.activity.get(), state)
// This states observes the live summary
// When fragment context will be destroyed the observer will automatically removed
liveSummary?.observe(lifecycleOwner, Observer {
it?.firstOrNull()?.let {
viewReactionViewModel.loadReaction()
}
})

return viewReactionViewModel
}


}
}

View File

@ -0,0 +1,23 @@
package im.vector.riotredesign.features.home.room.detail.timeline.action

import android.graphics.Typeface
import com.airbnb.epoxy.TypedEpoxyController

/**
* Epoxy controller for reaction event list
*/
class ViewReactionsEpoxyController(private val emojiCompatTypeface: Typeface?) : TypedEpoxyController<DisplayReactionsViewState>() {

override fun buildModels(state: DisplayReactionsViewState) {
val map = state.mapReactionKeyToMemberList() ?: return
map.forEach {
reactionInfoSimpleItem {
id(it.eventId)
emojiTypeFace(emojiCompatTypeface)
timeStamp(it.timestamp)
reactionKey(it.reactionKey)
authorDisplayName(it.authorName ?: it.authorId)
}
}
}
}

View File

@ -1,58 +0,0 @@
/*
* Copyright 2019 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.riotredesign.features.home.room.detail.timeline.factory

import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_

class CallItemFactory(private val stringProvider: StringProvider) {

fun create(event: TimelineEvent): NoticeItem? {
val text = buildNoticeText(event.root, event.senderName) ?: return null
return NoticeItem_()
.noticeText(text)
.avatarUrl(event.senderAvatar)
.memberName(event.senderName)
}

private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
return when {
EventType.CALL_INVITE == event.getClearType() -> {
val content = event.content.toModel<CallInviteContent>() ?: return null
val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
return if (isVideoCall) {
stringProvider.getString(R.string.notice_placed_video_call, senderName)
} else {
stringProvider.getString(R.string.notice_placed_voice_call, senderName)
}
}
EventType.CALL_ANSWER == event.getClearType() -> stringProvider.getString(R.string.notice_answered_call, senderName)
EventType.CALL_HANGUP == event.getClearType() -> stringProvider.getString(R.string.notice_ended_call, senderName)
else -> null
}

}


}

View File

@ -26,6 +26,9 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.resources.StringProvider import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_


// This class handles timeline event who haven't been successfully decrypted // This class handles timeline event who haven't been successfully decrypted
@ -34,24 +37,31 @@ class EncryptedItemFactory(private val stringProvider: StringProvider) {
fun create(timelineEvent: TimelineEvent): VectorEpoxyModel<*>? { fun create(timelineEvent: TimelineEvent): VectorEpoxyModel<*>? {
return when { return when {
EventType.ENCRYPTED == timelineEvent.root.getClearType() -> { EventType.ENCRYPTED == timelineEvent.root.getClearType() -> {
val cryptoError = timelineEvent.root.mCryptoError val cryptoError = timelineEvent.root.mCryptoError
val errorDescription = val errorDescription =
if (cryptoError?.code == MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE) { if (cryptoError?.code == MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE) {
stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id) stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id)
} else { } else {
cryptoError?.message cryptoError?.message
} }


val message = stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) val message = stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
val spannableStr = SpannableString(message) val spannableStr = SpannableString(message)
spannableStr.setSpan(StyleSpan(Typeface.ITALIC), 0, message.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannableStr.setSpan(StyleSpan(Typeface.ITALIC), 0, message.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// TODO This is not correct format for error, change it // TODO This is not correct format for error, change it
return NoticeItem_() val informationData = MessageInformationData(
.noticeText(spannableStr) eventId = timelineEvent.root.eventId ?: "?",
.avatarUrl(timelineEvent.senderAvatar) senderId = timelineEvent.root.sender ?: "",
.memberName(timelineEvent.senderName) sendState = timelineEvent.sendState,
avatarUrl = timelineEvent.senderAvatar(),
memberName = timelineEvent.senderName(),
showInformation = false
)
return NoticeItem_()
.noticeText(spannableStr)
.informationData(informationData)
} }
else -> null else -> null
} }
} }
} }

View File

@ -23,6 +23,9 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_


@ -30,10 +33,17 @@ class EncryptionItemFactory(private val stringProvider: StringProvider) {


fun create(event: TimelineEvent): NoticeItem? { fun create(event: TimelineEvent): NoticeItem? {
val text = buildNoticeText(event.root, event.senderName) ?: return null val text = buildNoticeText(event.root, event.senderName) ?: return null
val informationData = MessageInformationData(
eventId = event.root.eventId ?: "?",
senderId = event.root.sender ?: "",
sendState = event.sendState,
avatarUrl = event.senderAvatar(),
memberName = event.senderName(),
showInformation = false
)
return NoticeItem_() return NoticeItem_()
.noticeText(text) .noticeText(text)
.avatarUrl(event.senderAvatar) .informationData(informationData)
.memberName(event.senderName)
} }


private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {

View File

@ -23,7 +23,6 @@ import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan import android.text.style.RelativeSizeSpan
import android.view.View import android.view.View
import androidx.annotation.ColorRes
import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
@ -33,6 +32,7 @@ import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState 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.session.room.timeline.TimelineEvent
import im.vector.riotredesign.EmojiCompatFontProvider
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.extensions.localDateTime
@ -40,7 +40,6 @@ import im.vector.riotredesign.core.linkify.VectorLinkify
import im.vector.riotredesign.core.resources.ColorProvider import im.vector.riotredesign.core.resources.ColorProvider
import im.vector.riotredesign.core.resources.StringProvider import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.core.utils.DebouncedClickListener import im.vector.riotredesign.core.utils.DebouncedClickListener
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.getColorFromUserId import im.vector.riotredesign.features.home.getColorFromUserId
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
@ -55,7 +54,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val timelineDateFormatter: TimelineDateFormatter, private val timelineDateFormatter: TimelineDateFormatter,
private val htmlRenderer: EventHtmlRenderer, private val htmlRenderer: EventHtmlRenderer,
private val stringProvider: StringProvider) { private val stringProvider: StringProvider,
private val emojiCompatFontProvider: EmojiCompatFontProvider) {


fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
@ -115,24 +115,24 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
// val all = event.root.toContent() // val all = event.root.toContent()
// val ev = all.toModel<Event>() // val ev = all.toModel<Event>()
return when (messageContent) { return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
informationData, informationData,
hasBeenEdited, hasBeenEdited,
event.annotations?.editSummary, event.annotations?.editSummary,
callback) callback)
is MessageTextContent -> buildTextMessageItem(event.sendState, is MessageTextContent -> buildTextMessageItem(event.sendState,
messageContent, messageContent,
informationData, informationData,
hasBeenEdited, hasBeenEdited,
event.annotations?.editSummary, event.annotations?.editSummary,
callback callback
) )
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback)
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback) is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback) is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback)
else -> buildNotHandledMessageItem(messageContent) else -> buildNotHandledMessageItem(messageContent)
} }
} }


@ -141,23 +141,17 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
callback: TimelineEventController.Callback?): MessageFileItem? { callback: TimelineEventController.Callback?): MessageFileItem? {
return MessageFileItem_() return MessageFileItem_()
.informationData(informationData) .informationData(informationData)
.avatarCallback(callback)
.filename(messageContent.body) .filename(messageContent.body)
.iconRes(R.drawable.filetype_audio) .iconRes(R.drawable.filetype_audio)
.reactionPillCallback(callback) .reactionPillCallback(callback)
.avatarClickListener( .emojiTypeFace(emojiCompatFontProvider.typeface)
DebouncedClickListener(View.OnClickListener { view ->
callback?.onAvatarClicked(informationData)
}))
.memberClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onMemberNameClicked(informationData)
}))
.cellClickListener( .cellClickListener(
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view: View ->
callback?.onEventCellClicked(informationData, messageContent, view) callback?.onEventCellClicked(informationData, messageContent, view)
})) }))
.clickListener( .clickListener(
DebouncedClickListener(View.OnClickListener { _ -> DebouncedClickListener(View.OnClickListener {
callback?.onAudioMessageClicked(messageContent) callback?.onAudioMessageClicked(messageContent)
})) }))
.longClickListener { view -> .longClickListener { view ->
@ -171,17 +165,11 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
callback: TimelineEventController.Callback?): MessageFileItem? { callback: TimelineEventController.Callback?): MessageFileItem? {
return MessageFileItem_() return MessageFileItem_()
.informationData(informationData) .informationData(informationData)
.avatarCallback(callback)
.filename(messageContent.body) .filename(messageContent.body)
.reactionPillCallback(callback) .reactionPillCallback(callback)
.emojiTypeFace(emojiCompatFontProvider.typeface)
.iconRes(R.drawable.filetype_attachment) .iconRes(R.drawable.filetype_attachment)
.avatarClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onAvatarClicked(informationData)
}))
.memberClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onMemberNameClicked(informationData)
}))
.cellClickListener( .cellClickListener(
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view) callback?.onEventCellClicked(informationData, messageContent, view)
@ -219,16 +207,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return MessageImageVideoItem_() return MessageImageVideoItem_()
.playable(messageContent.info?.mimeType == "image/gif") .playable(messageContent.info?.mimeType == "image/gif")
.informationData(informationData) .informationData(informationData)
.avatarCallback(callback)
.mediaData(data) .mediaData(data)
.reactionPillCallback(callback) .reactionPillCallback(callback)
.avatarClickListener( .emojiTypeFace(emojiCompatFontProvider.typeface)
DebouncedClickListener(View.OnClickListener { view ->
callback?.onAvatarClicked(informationData)
}))
.memberClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onMemberNameClicked(informationData)
}))
.clickListener( .clickListener(
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view) callback?.onImageMessageClicked(messageContent, data, view)
@ -266,16 +248,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return MessageImageVideoItem_() return MessageImageVideoItem_()
.playable(true) .playable(true)
.informationData(informationData) .informationData(informationData)
.avatarCallback(callback)
.mediaData(thumbnailData) .mediaData(thumbnailData)
.reactionPillCallback(callback) .reactionPillCallback(callback)
.avatarClickListener( .emojiTypeFace(emojiCompatFontProvider.typeface)
DebouncedClickListener(View.OnClickListener { view ->
callback?.onAvatarClicked(informationData)
}))
.memberClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onMemberNameClicked(informationData)
}))
.cellClickListener( .cellClickListener(
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view) callback?.onEventCellClicked(informationData, messageContent, view)
@ -310,15 +286,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
} }
} }
.informationData(informationData) .informationData(informationData)
.avatarCallback(callback)
.reactionPillCallback(callback) .reactionPillCallback(callback)
.avatarClickListener( .emojiTypeFace(emojiCompatFontProvider.typeface)
DebouncedClickListener(View.OnClickListener { view -> //click on the text
callback?.onAvatarClicked(informationData)
}))
.memberClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onMemberNameClicked(informationData)
}))
.cellClickListener( .cellClickListener(
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view) callback?.onEventCellClicked(informationData, messageContent, view)
@ -378,11 +349,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return MessageTextItem_() return MessageTextItem_()
.message(message) .message(message)
.informationData(informationData) .informationData(informationData)
.avatarCallback(callback)
.reactionPillCallback(callback) .reactionPillCallback(callback)
.avatarClickListener( .emojiTypeFace(emojiCompatFontProvider.typeface)
DebouncedClickListener(View.OnClickListener { view ->
callback?.onAvatarClicked(informationData)
}))
.memberClickListener( .memberClickListener(
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view ->
callback?.onMemberNameClicked(informationData) callback?.onMemberNameClicked(informationData)
@ -417,15 +386,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
} }
} }
.informationData(informationData) .informationData(informationData)
.avatarCallback(callback)
.reactionPillCallback(callback) .reactionPillCallback(callback)
.avatarClickListener( .emojiTypeFace(emojiCompatFontProvider.typeface)
DebouncedClickListener(View.OnClickListener { view ->
callback?.onAvatarClicked(informationData)
}))
.memberClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onMemberNameClicked(informationData)
}))
.cellClickListener( .cellClickListener(
DebouncedClickListener(View.OnClickListener { view -> DebouncedClickListener(View.OnClickListener { view ->
callback?.onEventCellClicked(informationData, messageContent, view) callback?.onEventCellClicked(informationData, messageContent, view)
@ -440,14 +403,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
callback: TimelineEventController.Callback?): RedactedMessageItem? { callback: TimelineEventController.Callback?): RedactedMessageItem? {
return RedactedMessageItem_() return RedactedMessageItem_()
.informationData(informationData) .informationData(informationData)
.avatarClickListener( .avatarCallback(callback)
DebouncedClickListener(View.OnClickListener { view ->
callback?.onAvatarClicked(informationData)
}))
.memberClickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onMemberNameClicked(informationData)
}))
} }


private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence { private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence {

View File

@ -17,23 +17,32 @@
package im.vector.riotredesign.features.home.room.detail.timeline.factory package im.vector.riotredesign.features.home.room.detail.timeline.factory


import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_


class NoticeItemFactory(private val eventFormatter: NoticeEventFormatter) { class NoticeItemFactory(private val eventFormatter: NoticeEventFormatter) {


fun create(event: TimelineEvent): NoticeItem? { fun create(event: TimelineEvent,
callback: TimelineEventController.Callback?): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null val formattedText = eventFormatter.format(event) ?: return null
val senderName = event.senderName() val informationData = MessageInformationData(
val senderAvatar = event.senderAvatar() eventId = event.root.eventId ?: "?",
senderId = event.root.sender ?: "",
sendState = event.sendState,
avatarUrl = event.senderAvatar(),
memberName = event.senderName(),
showInformation = false
)


return NoticeItem_() return NoticeItem_()
.noticeText(formattedText) .noticeText(formattedText)
.avatarUrl(senderAvatar) .informationData(informationData)
.memberName(senderName) .baseCallback(callback)
} }





View File

@ -1,56 +0,0 @@
/*
* Copyright 2019 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.riotredesign.features.home.room.detail.timeline.factory

import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_


class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvider) {

fun create(event: TimelineEvent): NoticeItem? {
val noticeText = buildNoticeText(event.root, event.senderName) ?: return null
return NoticeItem_()
.noticeText(noticeText)
.avatarUrl(event.senderAvatar)
.memberName(event.senderName)
}

private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
val historyVisibility = event.content.toModel<RoomHistoryVisibilityContent>()?.historyVisibility
?: return null
val formattedVisibility = when (historyVisibility) {
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited)
RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined)
RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable)
}
return stringProvider.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility)
}


}


View File

@ -17,10 +17,16 @@
package im.vector.riotredesign.features.home.room.detail.timeline.factory package im.vector.riotredesign.features.home.room.detail.timeline.factory


import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageDefaultContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.EmptyItem_ import im.vector.riotredesign.core.epoxy.EmptyItem_
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem_
import timber.log.Timber import timber.log.Timber


class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
@ -43,7 +49,7 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
EventType.STATE_HISTORY_VISIBILITY, EventType.STATE_HISTORY_VISIBILITY,
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> noticeItemFactory.create(event) EventType.CALL_ANSWER -> noticeItemFactory.create(event, callback)


// Crypto // Crypto
EventType.ENCRYPTION -> encryptionItemFactory.create(event) EventType.ENCRYPTION -> encryptionItemFactory.create(event)
@ -53,9 +59,32 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER, EventType.STICKER,
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event) EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)

else -> { else -> {
Timber.w("Ignored event (type: ${event.root.type}") //These are just for debug to display hidden event, they should be filtered out in normal mode
null if (TimelineDisplayableEvents.DEBUG_HIDDEN_EVENT) {
val informationData = MessageInformationData(eventId = event.root.eventId
?: "?",
senderId = event.root.sender ?: "",
sendState = event.sendState,
time = "",
avatarUrl = null,
memberName = "",
showInformation = false
)
val messageContent = event.root.content.toModel<MessageContent>()
?: MessageDefaultContent("", "", null, null)
MessageTextItem_()
.informationData(informationData)
.message("{ \"type\": ${event.root.type} }")
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
} else {
Timber.w("Ignored event (type: ${event.root.type}")
null
}
} }
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -22,10 +22,14 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.extensions.localDateTime


object TimelineDisplayableEvents { object TimelineDisplayableEvents {


//Debug helper, to show invisible items in time line (reaction, redacts)
val DEBUG_HIDDEN_EVENT = BuildConfig.SHOW_HIDDEN_TIMELINE_EVENTS

val DISPLAYABLE_TYPES = listOf( val DISPLAYABLE_TYPES = listOf(
EventType.MESSAGE, EventType.MESSAGE,
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
@ -41,6 +45,11 @@ object TimelineDisplayableEvents {
EventType.STICKER, EventType.STICKER,
EventType.STATE_ROOM_CREATE EventType.STATE_ROOM_CREATE
) )

val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf(
EventType.REDACTION,
EventType.REACTION
)
} }


fun TimelineEvent.isDisplayable(): Boolean { fun TimelineEvent.isDisplayable(): Boolean {

View File

@ -16,6 +16,7 @@


package im.vector.riotredesign.features.home.room.detail.timeline.item package im.vector.riotredesign.features.home.room.detail.timeline.item


import android.graphics.Typeface
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -28,6 +29,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.DebouncedClickListener
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
@ -44,15 +46,26 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
@EpoxyAttribute @EpoxyAttribute
var cellClickListener: View.OnClickListener? = null var cellClickListener: View.OnClickListener? = null


@EpoxyAttribute
var avatarClickListener: View.OnClickListener? = null

@EpoxyAttribute @EpoxyAttribute
var memberClickListener: View.OnClickListener? = null var memberClickListener: View.OnClickListener? = null


@EpoxyAttribute
var emojiTypeFace: Typeface? = null

@EpoxyAttribute @EpoxyAttribute
var reactionPillCallback: TimelineEventController.ReactionPillCallback? = null var reactionPillCallback: TimelineEventController.ReactionPillCallback? = null


@EpoxyAttribute
var avatarCallback: TimelineEventController.AvatarCallback?= null

private val _avatarClickListener = DebouncedClickListener(View.OnClickListener {
avatarCallback?.onAvatarClicked(informationData)
})
private val _memberNameClickListener = DebouncedClickListener(View.OnClickListener {
avatarCallback?.onMemberNameClicked(informationData)
})


var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
override fun onReacted(reactionButton: ReactionButton) { override fun onReacted(reactionButton: ReactionButton) {
reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, true) reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, true)
@ -61,6 +74,10 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
override fun onUnReacted(reactionButton: ReactionButton) { override fun onUnReacted(reactionButton: ReactionButton) {
reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, false) reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, false)
} }

override fun onLongClick(reactionButton: ReactionButton) {
reactionPillCallback?.onLongClickOnReactionPill(informationData, reactionButton.reactionString)
}
} }


override fun bind(holder: H) { override fun bind(holder: H) {
@ -73,9 +90,9 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
width = size width = size
} }
holder.avatarImageView.visibility = View.VISIBLE holder.avatarImageView.visibility = View.VISIBLE
holder.avatarImageView.setOnClickListener(avatarClickListener) holder.avatarImageView.setOnClickListener(_avatarClickListener)
holder.memberNameView.visibility = View.VISIBLE holder.memberNameView.visibility = View.VISIBLE
holder.memberNameView.setOnClickListener(memberClickListener) holder.memberNameView.setOnClickListener(_memberNameClickListener)
holder.timeView.visibility = View.VISIBLE holder.timeView.visibility = View.VISIBLE
holder.timeView.text = informationData.time holder.timeView.text = informationData.time
holder.memberNameView.text = informationData.memberName holder.memberNameView.text = informationData.memberName
@ -108,7 +125,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
//clear all reaction buttons (but not the Flow helper!) //clear all reaction buttons (but not the Flow helper!)
holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true } holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true }
val idToRefInFlow = ArrayList<Int>() val idToRefInFlow = ArrayList<Int>()
informationData.orderedReactionList?.chunked(7)?.firstOrNull()?.forEachIndexed { index, reaction -> informationData.orderedReactionList?.chunked(8)?.firstOrNull()?.forEachIndexed { index, reaction ->
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton -> (holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
reactionButton.isVisible = true reactionButton.isVisible = true
reactionButton.reactedListener = reactionClickListener reactionButton.reactedListener = reactionClickListener
@ -116,6 +133,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
idToRefInFlow.add(reactionButton.id) idToRefInFlow.add(reactionButton.id)
reactionButton.reactionString = reaction.key reactionButton.reactionString = reaction.key
reactionButton.reactionCount = reaction.count reactionButton.reactionCount = reaction.count
reactionButton.emojiTypeFace = emojiTypeFace
reactionButton.setChecked(reaction.addedByMe) reactionButton.setChecked(reaction.addedByMe)
reactionButton.isEnabled = reaction.synced reactionButton.isEnabled = reaction.synced
} }

View File

@ -23,6 +23,7 @@ import androidx.core.widget.TextViewCompat
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.containsOnlyEmojis
import im.vector.riotredesign.features.html.PillImageSpan import im.vector.riotredesign.features.html.PillImageSpan
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -51,12 +52,20 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {


override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)

holder.messageView.movementMethod = mvmtMethod holder.messageView.movementMethod = mvmtMethod



val msg = message ?: ""
if (msg.length <= 4 && containsOnlyEmojis(msg.toString())) {
holder.messageView.textSize = 44F
} else {
holder.messageView.textSize = 14F
}

val textFuture = PrecomputedTextCompat.getTextFuture(message ?: "", val textFuture = PrecomputedTextCompat.getTextFuture(message ?: "",
TextViewCompat.getTextMetricsParams(holder.messageView), TextViewCompat.getTextMetricsParams(holder.messageView),
null) null)

holder.messageView.setTextFuture(textFuture) holder.messageView.setTextFuture(textFuture)
holder.messageView.renderSendState() holder.messageView.renderSendState()
holder.messageView.setOnClickListener(cellClickListener) holder.messageView.setOnClickListener(cellClickListener)

View File

@ -23,27 +23,36 @@ import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController


@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() { abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {


@EpoxyAttribute @EpoxyAttribute
var noticeText: CharSequence? = null var noticeText: CharSequence? = null
@EpoxyAttribute
var avatarUrl: String? = null
@EpoxyAttribute
var userId: String = ""
@EpoxyAttribute
var memberName: CharSequence? = null



@EpoxyAttribute @EpoxyAttribute
var longClickListener: View.OnLongClickListener? = null lateinit var informationData: MessageInformationData

@EpoxyAttribute
var baseCallback: TimelineEventController.BaseCallback? = null

private var longClickListener = View.OnLongClickListener {
baseCallback?.onEventLongClicked(informationData, null, it)
baseCallback != null
}



override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.noticeTextView.text = noticeText holder.noticeTextView.text = noticeText
AvatarRenderer.render(avatarUrl, userId, memberName?.toString(), holder.avatarImageView) AvatarRenderer.render(
informationData.avatarUrl,
informationData.senderId,
informationData.memberName?.toString()
?: informationData.senderId,
holder.avatarImageView
)
holder.view.setOnLongClickListener(longClickListener) holder.view.setOnLongClickListener(longClickListener)
} }


@ -51,7 +60,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {


class Holder : BaseHolder() { class Holder : BaseHolder() {
override fun getStubId(): Int = STUB_ID override fun getStubId(): Int = STUB_ID

val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView) val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView) val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
} }

View File

@ -72,7 +72,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
setupRecyclerView() setupRecyclerView()
roomListViewModel.subscribe { renderState(it) } roomListViewModel.subscribe { renderState(it) }
roomListViewModel.openRoomLiveData.observeEvent(this) { roomListViewModel.openRoomLiveData.observeEvent(this) {
navigator.openRoom(it) navigator.openRoom(it, requireActivity())
} }


createChatFabMenu.listener = this createChatFabMenu.listener = this
@ -116,7 +116,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O




override fun openRoomDirectory() { override fun openRoomDirectory() {
navigator.openRoomDirectory() navigator.openRoomDirectory(requireActivity())
} }


override fun createDirectChat() { override fun createDirectChat() {

View File

@ -17,6 +17,7 @@
package im.vector.riotredesign.features.navigation package im.vector.riotredesign.features.navigation


import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
@ -27,32 +28,31 @@ import im.vector.riotredesign.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotredesign.features.roomdirectory.roompreview.RoomPreviewActivity import im.vector.riotredesign.features.roomdirectory.roompreview.RoomPreviewActivity
import im.vector.riotredesign.features.settings.VectorSettingsActivity import im.vector.riotredesign.features.settings.VectorSettingsActivity


class DefaultNavigator(private val fraqment: Fragment) : Navigator { class DefaultNavigator : Navigator {


val activity: Activity = fraqment.requireActivity()


override fun openRoom(roomId: String) { override fun openRoom(roomId: String, context: Context) {
val args = RoomDetailArgs(roomId) val args = RoomDetailArgs(roomId)
val intent = RoomDetailActivity.newIntent(activity, args) val intent = RoomDetailActivity.newIntent(context, args)
activity.startActivity(intent) context.startActivity(intent)
} }


override fun openRoomPreview(publicRoom: PublicRoom) { override fun openRoomPreview(publicRoom: PublicRoom, context: Context) {
val intent = RoomPreviewActivity.getIntent(activity, publicRoom) val intent = RoomPreviewActivity.getIntent(context, publicRoom)
activity.startActivity(intent) context.startActivity(intent)
} }


override fun openRoomDirectory() { override fun openRoomDirectory(context: Context) {
val intent = Intent(activity, RoomDirectoryActivity::class.java) val intent = Intent(context, RoomDirectoryActivity::class.java)
activity.startActivity(intent) context.startActivity(intent)
} }


override fun openSettings() { override fun openSettings(context: Context) {
val intent = VectorSettingsActivity.getIntent(activity, "TODO") val intent = VectorSettingsActivity.getIntent(context, "TODO")
activity.startActivity(intent) context.startActivity(intent)
} }


override fun openDebug() { override fun openDebug(context: Context) {
activity.startActivity(Intent(activity, DebugMenuActivity::class.java)) context.startActivity(Intent(context, DebugMenuActivity::class.java))
} }
} }

View File

@ -16,18 +16,19 @@


package im.vector.riotredesign.features.navigation package im.vector.riotredesign.features.navigation


import android.content.Context
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom


interface Navigator { interface Navigator {


fun openRoom(roomId: String) fun openRoom(roomId: String, context: Context)


fun openRoomPreview(publicRoom: PublicRoom) fun openRoomPreview(publicRoom: PublicRoom, context: Context)


fun openRoomDirectory() fun openRoomDirectory(context: Context)


fun openSettings() fun openSettings(context: Context)


fun openDebug() fun openDebug(context: Context)


} }

View File

@ -19,23 +19,20 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Handler
import android.os.HandlerThread
import android.util.TypedValue import android.util.TypedValue
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.widget.SearchView import android.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.provider.FontRequest
import androidx.core.provider.FontsContractCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import im.vector.riotredesign.EmojiCompatFontProvider
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseActivity import im.vector.riotredesign.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.* import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.*
import timber.log.Timber import org.koin.android.ext.android.inject


/** /**
* *
@ -44,20 +41,21 @@ import timber.log.Timber
* TODO: Finish Refactor to vector base activity * TODO: Finish Refactor to vector base activity
* TODO: Move font request to app * TODO: Move font request to app
*/ */
class EmojiReactionPickerActivity : VectorBaseActivity() { class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvider.FontProviderListener {



private lateinit var tabLayout: TabLayout private lateinit var tabLayout: TabLayout


lateinit var viewModel: EmojiChooserViewModel lateinit var viewModel: EmojiChooserViewModel


private var mHandler: Handler? = null

override fun getMenuRes(): Int = R.menu.menu_emoji_reaction_picker override fun getMenuRes(): Int = R.menu.menu_emoji_reaction_picker


override fun getLayoutRes(): Int = R.layout.activity_emoji_reaction_picker override fun getLayoutRes(): Int = R.layout.activity_emoji_reaction_picker


override fun getTitleRes(): Int = R.string.title_activity_emoji_reaction_picker override fun getTitleRes(): Int = R.string.title_activity_emoji_reaction_picker


val emojiCompatFontProvider by inject<EmojiCompatFontProvider>()

private var tabLayoutSelectionListener = object : TabLayout.BaseOnTabSelectedListener<TabLayout.Tab> { private var tabLayoutSelectionListener = object : TabLayout.BaseOnTabSelectedListener<TabLayout.Tab> {
override fun onTabReselected(p0: TabLayout.Tab) { override fun onTabReselected(p0: TabLayout.Tab) {
} }
@ -71,19 +69,13 @@ class EmojiReactionPickerActivity : VectorBaseActivity() {


} }


private fun getFontThreadHandler(): Handler {
if (mHandler == null) {
val handlerThread = HandlerThread("fonts")
handlerThread.start()
mHandler = Handler(handlerThread.looper)
}
return mHandler!!
}

override fun initUiAndData() { override fun initUiAndData() {
configureToolbar(emojiPickerToolbar) configureToolbar(emojiPickerToolbar)


requestEmojivUnicode10CompatibleFont() emojiCompatFontProvider.let {
EmojiDrawView.configureTextPaint(this, it.typeface)
it.addListener(this)
}


tabLayout = findViewById(R.id.tabs) tabLayout = findViewById(R.id.tabs)


@ -124,27 +116,13 @@ class EmojiReactionPickerActivity : VectorBaseActivity() {
}) })
} }


private fun requestEmojivUnicode10CompatibleFont() { override fun compatibilityFontUpdate(typeface: Typeface?) {
val fontRequest = FontRequest( EmojiDrawView.configureTextPaint(this, typeface)
"com.google.android.gms.fonts", }
"com.google.android.gms",
"Noto Color Emoji Compat",
R.array.com_google_android_gms_fonts_certs
)


EmojiDrawView.configureTextPaint(this, null) override fun onDestroy() {
val callback = object : FontsContractCompat.FontRequestCallback() { emojiCompatFontProvider.removeListener(this)

super.onDestroy()
override fun onTypefaceRetrieved(typeface: Typeface) {
EmojiDrawView.configureTextPaint(this@EmojiReactionPickerActivity, typeface)
}

override fun onTypefaceRequestFailed(reason: Int) {
Timber.e("Failed to load Emoji Compatible font, reason:$reason")
}
}

FontsContractCompat.requestFont(this, fontRequest, callback, getFontThreadHandler())
} }


override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@ -21,10 +21,10 @@ import android.animation.AnimatorSet
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.content.Context import android.content.Context
import android.content.res.TypedArray import android.content.res.TypedArray
import android.graphics.Typeface
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
@ -36,13 +36,15 @@ import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.TextUtils


/** /**
* An animated reaction button. * An animated reaction button.
* Displays a String reaction (emoji), with a count, and that can be selected or not (toggle) * Displays a String reaction (emoji), with a count, and that can be selected or not (toggle)
*/ */
class ReactionButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, class ReactionButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr), View.OnClickListener { defStyleAttr: Int = 0)
: FrameLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener {


companion object { companion object {
private val DECCELERATE_INTERPOLATOR = DecelerateInterpolator() private val DECCELERATE_INTERPOLATOR = DecelerateInterpolator()
@ -56,6 +58,11 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut


private var reactionSelector: View? = null private var reactionSelector: View? = null


var emojiTypeFace: Typeface? = null
set(value) {
field = value
emojiView?.typeface = value ?: Typeface.DEFAULT
}


private var dotsView: DotsView private var dotsView: DotsView
private var circleView: CircleView private var circleView: CircleView
@ -68,7 +75,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
var reactionCount = 11 var reactionCount = 11
set(value) { set(value) {
field = value field = value
countTextView?.text = value.toString() countTextView?.text = TextUtils.formatCountToShortDecimal(value)
} }




@ -95,7 +102,9 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
reactionSelector = findViewById(R.id.reactionSelector) reactionSelector = findViewById(R.id.reactionSelector)
countTextView = findViewById(R.id.reactionCount) countTextView = findViewById(R.id.reactionCount)


countTextView?.text = reactionCount.toString() countTextView?.text = TextUtils.formatCountToShortDecimal(reactionCount)

emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT


val array = context.obtainStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr, 0) val array = context.obtainStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr, 0)


@ -128,6 +137,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
val status = array.getBoolean(R.styleable.ReactionButton_toggled, false) val status = array.getBoolean(R.styleable.ReactionButton_toggled, false)
setChecked(status) setChecked(status)
setOnClickListener(this) setOnClickListener(this)
setOnLongClickListener(this)
array.recycle() array.recycle()
} }


@ -234,40 +244,45 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
* @param event * @param event
* @return * @return
*/ */
override fun onTouchEvent(event: MotionEvent): Boolean { // override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isEnabled) // if (!isEnabled)
return true // return true
//
// when (event.action) {
// MotionEvent.ACTION_DOWN ->
// /*
// Commented out this line and moved the animation effect to the action up event due to
// conflicts that were occurring when library is used in sliding type views.
//
// icon.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).setInterpolator(DECCELERATE_INTERPOLATOR);
// */
// isPressed = true
//
// MotionEvent.ACTION_MOVE -> {
// val x = event.x
// val y = event.y
// val isInside = x > 0 && x < width && y > 0 && y < height
// if (isPressed != isInside) {
// isPressed = isInside
// }
// }
//
// MotionEvent.ACTION_UP -> {
// emojiView!!.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).interpolator = DECCELERATE_INTERPOLATOR
// emojiView!!.animate().scaleX(1f).scaleY(1f).interpolator = DECCELERATE_INTERPOLATOR
// if (isPressed) {
// performClick()
// isPressed = false
// }
// }
// MotionEvent.ACTION_CANCEL -> isPressed = false
// }
// return true
// }


when (event.action) { override fun onLongClick(v: View?): Boolean {
MotionEvent.ACTION_DOWN -> reactedListener?.onLongClick(this)
/* return reactedListener != null
Commented out this line and moved the animation effect to the action up event due to
conflicts that were occurring when library is used in sliding type views.

icon.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).setInterpolator(DECCELERATE_INTERPOLATOR);
*/
isPressed = true

MotionEvent.ACTION_MOVE -> {
val x = event.x
val y = event.y
val isInside = x > 0 && x < width && y > 0 && y < height
if (isPressed != isInside) {
isPressed = isInside
}
}

MotionEvent.ACTION_UP -> {
emojiView!!.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).interpolator = DECCELERATE_INTERPOLATOR
emojiView!!.animate().scaleX(1f).scaleY(1f).interpolator = DECCELERATE_INTERPOLATOR
if (isPressed) {
performClick()
isPressed = false
}
}
MotionEvent.ACTION_CANCEL -> isPressed = false
}
return true
} }


/** /**
@ -327,5 +342,6 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
interface ReactedListener { interface ReactedListener {
fun onReacted(reactionButton: ReactionButton) fun onReacted(reactionButton: ReactionButton)
fun onUnReacted(reactionButton: ReactionButton) fun onUnReacted(reactionButton: ReactionButton)
fun onLongClick(reactionButton: ReactionButton)
} }
} }

View File

@ -124,12 +124,12 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback


when (joinState) { when (joinState) {
JoinState.JOINED -> { JoinState.JOINED -> {
navigator.openRoom(publicRoom.roomId) navigator.openRoom(publicRoom.roomId, requireActivity())
} }
JoinState.NOT_JOINED, JoinState.NOT_JOINED,
JoinState.JOINING_ERROR -> { JoinState.JOINING_ERROR -> {
// ROOM PREVIEW // ROOM PREVIEW
navigator.openRoomPreview(publicRoom) navigator.openRoomPreview(publicRoom, requireActivity())
} }
else -> { else -> {
Snackbar.make(publicRoomsCoordinator, getString(R.string.please_wait), Snackbar.LENGTH_SHORT) Snackbar.make(publicRoomsCoordinator, getString(R.string.please_wait), Snackbar.LENGTH_SHORT)

View File

@ -108,7 +108,7 @@ class RoomPreviewNoPreviewFragment : VectorBaseFragment() {
// Quit this screen // Quit this screen
requireActivity().finish() requireActivity().finish()
// Open room // Open room
navigator.openRoom(roomPreviewData.roomId) navigator.openRoom(roomPreviewData.roomId, requireActivity())
} }
} }
} }

View File

@ -2322,7 +2322,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
} }


// add the edit text preference // add the edit text preference
preference = VectorPreference(activity!!).apply { preference = VectorPreference(requireActivity()).apply {
mTypeface = typeFaceHighlight mTypeface = typeFaceHighlight
} }



View File

@ -0,0 +1,38 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M11,11m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#9e9e9e"
android:strokeLineCap="round"/>
<path
android:pathData="m7,13s1.5,2 4,2 4,-2 4,-2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#9e9e9e"
android:strokeLineCap="round"/>
<path
android:pathData="m8,8h0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#9e9e9e"
android:strokeLineCap="round"/>
<path
android:pathData="m14,8h0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#9e9e9e"
android:strokeLineCap="round"/>
</vector>

View File

@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">


<size android:width="40dp" android:height="22dp"/> <!--<size android:width="40dp" android:height="22dp"/>-->


<solid android:color="?vctr_list_header_background_color" /> <solid android:color="?vctr_list_header_background_color" />



View File

@ -15,25 +15,23 @@
tools:layout="@layout/emoji_chooser_fragment" /> tools:layout="@layout/emoji_chooser_fragment" />


<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
style="@style/VectorAppBarLayoutStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:elevation="4dp">


<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/emojiPickerToolbar" android:id="@+id/emojiPickerToolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:minHeight="0dp" android:minHeight="0dp"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar" app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways" /> tools:title="@string/reactions" />


<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tabs" android:id="@+id/tabs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="40dp" android:layout_height="40dp" />
android:background="?attr/colorPrimary"
android:elevation="4dp" />


</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>



View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="400dp"
android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="44dp"
android:gravity="center_vertical"
android:padding="8dp"
android:text="@string/reactions"
android:textColor="?android:textColorSecondary"
android:textSize="16sp" />

<ProgressBar
android:id="@+id/bottomSheetViewReactionSpinner"
style="?android:attr/progressBarStyleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:visibility="gone"
tools:visibility="visible" />


<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/bottom_sheet_display_reactions_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fadeScrollbars="false"
android:orientation="vertical"
android:scrollbars="vertical"
tools:itemCount="15"
tools:listitem="@layout/item_simple_reaction_info">

</com.airbnb.epoxy.EpoxyRecyclerView>
</LinearLayout>

View File

@ -86,7 +86,8 @@
tools:text="Friday 8pm" /> tools:text="Friday 8pm" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>


<LinearLayout <View
android:id="@+id/quickReactTopDivider"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color" /> android:background="?attr/vctr_list_divider_color" />
@ -94,18 +95,22 @@
<FrameLayout <FrameLayout
android:id="@+id/bottom_sheet_quick_reaction_container" android:id="@+id/bottom_sheet_quick_reaction_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content"
tools:background="@android:color/holo_green_light"
tools:layout_height="180dp" />


<LinearLayout <View
android:id="@+id/quickReactBottomDivider"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color" /> android:background="?attr/vctr_list_divider_color" />



<FrameLayout <FrameLayout
android:id="@+id/bottom_sheet_menu_container" android:id="@+id/bottom_sheet_menu_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content"
tools:background="@android:color/holo_blue_dark"
tools:layout_height="250dp" />


</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="44dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingLeft="8dp"
android:paddingEnd="8dp">

<TextView
android:id="@+id/itemSimpleReactionInfoKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:gravity="center"
android:lines="1"
android:textColor="?android:textColorPrimary"
android:textSize="18sp"
tools:text="@sample/reactions.json/data/reaction" />

<TextView
android:id="@+id/itemSimpleReactionInfoMemberName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:layout_weight="1"
android:ellipsize="end"
android:lines="1"
android:textColor="?android:textColorPrimary"
android:textSize="16sp"
tools:text="@sample/matrix.json/data/displayName" />

<TextView
android:id="@+id/itemSimpleReactionInfoTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lines="1"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
tools:text="10:44" />


</LinearLayout>

View File

@ -4,6 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:addStatesFromChildren="true" android:addStatesFromChildren="true"
android:background="?attr/selectableItemBackground"
android:paddingLeft="8dp" android:paddingLeft="8dp"
android:paddingRight="8dp"> android:paddingRight="8dp">


@ -31,9 +32,9 @@
<ViewStub <ViewStub
android:id="@+id/messageContentBlankStub" android:id="@+id/messageContentBlankStub"
style="@style/TimelineContentStubNoInfoLayoutParams" style="@style/TimelineContentStubNoInfoLayoutParams"
android:layout="@layout/item_timeline_event_blank_stub"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout="@layout/item_timeline_event_blank_stub"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints" />


<ViewStub <ViewStub

View File

@ -2,16 +2,19 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="44dp" android:id="@+id/reactionSelector"
android:layout_width="wrap_content"
android:minWidth="44dp"
android:layout_height="26dp" android:layout_height="26dp"
android:background="@drawable/rounded_rect_shape"
android:clipChildren="false"> android:clipChildren="false">




<View <!--<View-->
android:id="@+id/reactionSelector" <!--android:id="@+id/reactionSelector"-->
android:layout_width="match_parent" <!--android:layout_width="match_parent"-->
android:layout_height="match_parent" <!--android:layout_height="match_parent"-->
android:background="@drawable/rounded_rect_shape" /> <!--android:background="@drawable/rounded_rect_shape" />-->


<im.vector.riotredesign.features.reactions.widget.DotsView <im.vector.riotredesign.features.reactions.widget.DotsView
android:id="@+id/dots" android:id="@+id/dots"
@ -42,17 +45,23 @@
android:gravity="center" android:gravity="center"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="13sp" android:textSize="13sp"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/reactionCount"
tools:text="👍" /> tools:text="👍" />


<TextView <TextView
android:id="@+id/reactionCount" android:id="@+id/reactionCount"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_marginEnd="6dp" app:layout_constraintHorizontal_chainStyle="packed"
android:layout_marginRight="6dp" app:layout_constraintBaseline_toBaselineOf="@id/reactionText"
android:layout_marginStart="-4dp"
android:layout_marginLeft="-4dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:gravity="center" android:gravity="center"
android:maxLines="1" android:maxLines="1"
android:textColor="?riotx_text_secondary" android:textColor="?riotx_text_secondary"
@ -61,7 +70,8 @@
app:autoSizeMaxTextSize="14sp" app:autoSizeMaxTextSize="14sp"
app:autoSizeMinTextSize="8sp" app:autoSizeMinTextSize="8sp"
app:autoSizeTextType="uniform" app:autoSizeTextType="uniform"
app:layout_constraintStart_toEndOf="@id/reactionText"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
tools:text="10" /> tools:text="13450" />


</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -20,6 +20,8 @@
<string name="reactions_agree">Agree</string> <string name="reactions_agree">Agree</string>
<string name="reactions_like">Like</string> <string name="reactions_like">Like</string>
<string name="message_add_reaction">Add Reaction</string> <string name="message_add_reaction">Add Reaction</string>
<string name="message_view_reaction">View Reactions</string>
<string name="reactions">Reactions</string>


<string name="event_redacted_by_user_reason">Event deleted by user</string> <string name="event_redacted_by_user_reason">Event deleted by user</string>
<string name="event_redacted_by_admin_reason">Event moderated by room admin</string> <string name="event_redacted_by_admin_reason">Event moderated by room admin</string>

View File

@ -26,6 +26,10 @@
<item name="android:fontFamily">"sans-serif"</item> <item name="android:fontFamily">"sans-serif"</item>
</style> </style>


<style name="VectorAppBarLayoutStyle" parent="Widget.Design.AppBarLayout">
<item name="android:background">?riotx_background</item>
</style>

<!-- Alert Dialog: Button color are not colorAccent by default --> <!-- Alert Dialog: Button color are not colorAccent by default -->
<style name="VectorAlertDialogStyleLight" parent="Theme.MaterialComponents.Light.Dialog.Alert"> <style name="VectorAlertDialogStyleLight" parent="Theme.MaterialComponents.Light.Dialog.Alert">
<item name="buttonBarButtonStyle">@style/VectorAlertDialogButtonStyle</item> <item name="buttonBarButtonStyle">@style/VectorAlertDialogButtonStyle</item>