mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-09 00:29:00 +01:00
Merge pull request #168 from vector-im/feature/aggregation_p1_wrapup
Feature/aggregation p1 wrapup
This commit is contained in:
commit
8f2c005d82
@ -48,7 +48,7 @@ android {
|
|||||||
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.BASIC"
|
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
|
||||||
}
|
}
|
||||||
|
|
||||||
release {
|
release {
|
||||||
|
@ -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>>
|
||||||
}
|
}
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 fun execute(params: EventRelationsAggregationTask.Params): Try<Unit> {
|
override 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}")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
22
vector/sampledata/reactions.json
Normal file
22
vector/sampledata/reactions.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"reaction" : "👍"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reaction" : "😀"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reaction" : "😞"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reaction" : "Not a reaction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reaction" : "✅"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reaction" : "🎉"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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?)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
@ -41,6 +45,10 @@ import timber.log.Timber
|
|||||||
|
|
||||||
class VectorApplication : Application() {
|
class VectorApplication : Application() {
|
||||||
|
|
||||||
|
//font thread handler
|
||||||
|
private var mFontThreadHandler: Handler? = null
|
||||||
|
|
||||||
|
|
||||||
val vectorConfiguration: VectorConfiguration by inject()
|
val vectorConfiguration: VectorConfiguration by inject()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@ -63,10 +71,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)
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,4 +99,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!!
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
@ -86,8 +86,12 @@ class AppModule(private val context: Context) {
|
|||||||
Matrix.getInstance().currentSession!!
|
Matrix.getInstance().currentSession!!
|
||||||
}
|
}
|
||||||
|
|
||||||
factory { (fragment: Fragment) ->
|
factory {
|
||||||
DefaultNavigator(fragment) as Navigator
|
DefaultNavigator() as Navigator
|
||||||
|
}
|
||||||
|
|
||||||
|
single {
|
||||||
|
EmojiCompatFontProvider()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -72,7 +72,8 @@ class HomeModule {
|
|||||||
val timelineMediaSizeProvider = TimelineMediaSizeProvider()
|
val timelineMediaSizeProvider = TimelineMediaSizeProvider()
|
||||||
val colorProvider = ColorProvider(fragment.requireContext())
|
val colorProvider = ColorProvider(fragment.requireContext())
|
||||||
val timelineDateFormatter = get<TimelineDateFormatter>()
|
val timelineDateFormatter = get<TimelineDateFormatter>()
|
||||||
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,
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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>()
|
||||||
|
@ -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
|
||||||
|
@ -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?
|
||||||
|
@ -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()
|
||||||
|
@ -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(
|
||||||
@ -122,9 +126,9 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
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.type == 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 +137,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.type != EventType.MESSAGE) return false
|
if (event.root.type != 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,
|
||||||
@ -144,14 +148,19 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
MessageType.MSGTYPE_VIDEO,
|
MessageType.MSGTYPE_VIDEO,
|
||||||
MessageType.MSGTYPE_AUDIO,
|
MessageType.MSGTYPE_AUDIO,
|
||||||
MessageType.MSGTYPE_FILE -> true
|
MessageType.MSGTYPE_FILE -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.type == 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.type != EventType.MESSAGE) return false
|
if (event.root.type != 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,
|
||||||
@ -159,7 +168,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
MessageType.MSGTYPE_LOCATION -> {
|
MessageType.MSGTYPE_LOCATION -> {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +179,13 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
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.type != 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.type != EventType.MESSAGE) return false
|
||||||
@ -182,7 +198,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,
|
||||||
@ -191,19 +207,19 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
|
|||||||
MessageType.MSGTYPE_LOCATION -> {
|
MessageType.MSGTYPE_LOCATION -> {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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,
|
||||||
MessageType.MSGTYPE_VIDEO -> {
|
MessageType.MSGTYPE_VIDEO -> {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,9 +233,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"
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
@ -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 }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
// Unhandled event types (yet)
|
// Unhandled event types (yet)
|
||||||
EventType.ENCRYPTED,
|
EventType.ENCRYPTED,
|
||||||
@ -51,9 +57,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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
38
vector/src/main/res/drawable/ic_view_reactions.xml
Normal file
38
vector/src/main/res/drawable/ic_view_reactions.xml
Normal 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>
|
@ -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" />
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
47
vector/src/main/res/layout/item_simple_reaction_info.xml
Normal file
47
vector/src/main/res/layout/item_simple_reaction_info.xml
Normal 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>
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user