diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 4d12a47922..f2c3cb2b1d 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -48,7 +48,7 @@ android { // TODO Set to false buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" // Set to BODY instead of NONE to enable logging - buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.HEADERS" + buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE" } release { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index bc92472892..ac08b64f53 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -15,7 +15,9 @@ */ 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.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.util.Cancelable /** @@ -91,4 +93,5 @@ interface RelationService { */ fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? + fun getEventSummaryLive(eventId: String): LiveData> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index fb163dae7a..8d5fe49ebc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -16,6 +16,7 @@ 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.UnsignedData import im.vector.matrix.android.internal.database.model.EventEntity @@ -46,8 +47,16 @@ internal object EventMapper { fun map(eventEntity: EventEntity): Event { //TODO proxy the event to only parse unsigned data when accessed? - var ud = if (eventEntity.unsignedData.isNullOrBlank()) null - else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(eventEntity.unsignedData) + val ud = if (eventEntity.unsignedData.isNullOrBlank()) { + null + } else { + try { + MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(eventEntity.unsignedData) + } catch (t: JsonDataException) { + null + } + + } return Event( type = eventEntity.type, eventId = eventEntity.eventId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index 38f4f4518c..5b3e869058 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -19,12 +19,10 @@ package im.vector.matrix.android.internal.database.model import im.vector.matrix.android.api.session.room.send.SendState import io.realm.RealmObject import io.realm.RealmResults -import io.realm.annotations.Ignore import io.realm.annotations.Index import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey import java.util.* -import kotlin.properties.Delegates internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(), @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 - @delegate:Ignore - var sendState: SendState by Delegates.observable(SendState.valueOf(sendStateStr)) { _, _, newValue -> - sendStateStr = newValue.name - } + var sendState: SendState + get() { + return SendState.valueOf(sendStateStr) + } + set(value) { + sendStateStr = value.name + } + companion object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index 275c4d02a2..e12d60cac2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -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.where 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 timber.log.Timber @@ -44,61 +44,79 @@ internal interface EventRelationsAggregationTask : Task { - return monarchy.tryTransactionAsync { realm -> - update(realm, params.events, params.userId) + val events = params.events + 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>, userId: String) { events.forEach { pair -> - val roomId = pair.first.roomId ?: return@forEach - val event = pair.first - val sendState = pair.second - val isLocalEcho = sendState == SendState.UNSENT - when (event.type) { - EventType.REACTION -> { - //we got a reaction!! - Timber.v("###REACTION in room $roomId") - handleReaction(event, roomId, realm, userId, isLocalEcho) + try { //Temporary catch, should be removed + val roomId = pair.first.roomId + if (roomId == null) { + Timber.w("Event has no room id ${pair.first.eventId}") + return@forEach } - EventType.MESSAGE -> { - if (event.unsignedData?.relations?.annotations != null) { - Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") - handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) - } else { - val content: MessageContent? = event.content.toModel() - if (content?.relatesTo?.type == RelationType.REPLACE) { - Timber.v("###REPLACE in room $roomId for event ${event.eventId}") - //A replace! - handleReplace(realm, event, content, roomId, isLocalEcho) - } + val event = pair.first + val sendState = pair.second + val isLocalEcho = sendState == SendState.UNSENT + when (event.type) { + EventType.REACTION -> { + //we got a reaction!! + Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") + handleReaction(event, roomId, realm, userId, isLocalEcho) } - - } - EventType.REDACTION -> { - val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } - ?: return - 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() - if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { - handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) + EventType.MESSAGE -> { + if (event.unsignedData?.relations?.annotations != null) { + Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") + handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) + } else { + val content: MessageContent? = event.content.toModel() + if (content?.relatesTo?.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + //A replace! + handleReplace(realm, event, content, roomId, isLocalEcho) } - } - 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() + 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) { @@ -108,7 +126,7 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc //ok, this is a replace var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() 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.roomId = roomId } @@ -116,7 +134,7 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc //we have it val existingSummary = existing.editSummary 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 val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java) 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) { - aggregation.chunk?.forEach { - if (it.type == EventType.REACTION) { - val eventId = event.eventId ?: "" - val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() - if (existing == null) { - val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId) - eventSummary.roomId = roomId - val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) - sum.key = it.key - sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order? - sum.count = it.count - eventSummary.reactionsSummary.add(sum) - } else { - //TODO how to handle that + if (SHOULD_HANDLE_SERVER_AGREGGATION) { + aggregation.chunk?.forEach { + if (it.type == EventType.REACTION) { + val eventId = event.eventId ?: "" + val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + if (existing == null) { + val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId) + eventSummary.roomId = roomId + val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = it.key + sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order? + sum.count = it.count + eventSummary.reactionsSummary.add(sum) + } else { + //TODO how to handle that + } } } } } private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) { - event.content.toModel()?.let { content -> - //rel_type must be m.annotation - if (RelationType.ANNOTATION == content.relatesTo?.type) { - val reaction = content.relatesTo.key - val eventId = content.relatesTo.eventId - val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() - ?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId } + val content = event.content.toModel() + if (content == null) { + Timber.e("Malformed reaction content ${event.content}") + return + } + //rel_type must be m.annotation + 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 } - val txId = event.unsignedData?.transactionId - if (isLocalEcho && txId.isNullOrBlank()) { - Timber.w("Received a local echo with no transaction ID") - } - if (sum == null) { - sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) - sum.key = reaction - sum.firstTimestamp = event.originServerTs ?: 0 - if (isLocalEcho) { - Timber.v("Adding local echo reaction $reaction") - sum.sourceLocalEcho.add(txId) - 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) + var sum = eventSummary.reactionsSummary.find { it.key == reaction } + val txId = event.unsignedData?.transactionId + if (isLocalEcho && txId.isNullOrBlank()) { + Timber.w("Received a local echo with no transaction ID") + } + if (sum == null) { + sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = reaction + sum.firstTimestamp = event.originServerTs ?: 0 + if (isLocalEcho) { + Timber.v("Adding local echo reaction $reaction") + sum.sourceLocalEcho.add(txId) + sum.count = 1 } else { - //is this a known event (is possible? pagination?) - if (!sum.sourceEvents.contains(eventId)) { + Timber.v("Adding synced reaction $reaction") + 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 - if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) { - //ok it has already been counted, just sync the list, do not touch count - Timber.v("Ignoring synced of local echo for reaction $reaction") - sum.sourceLocalEcho.remove(txId) - sum.sourceEvents.add(event.eventId) + //check if it's not the sync of a local echo + if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) { + //ok it has already been counted, just sync the list, do not touch count + Timber.v("Ignoring synced of local echo for reaction $reaction") + sum.sourceLocalEcho.remove(txId) + sum.sourceEvents.add(reactionEventId) + } else { + sum.count += 1 + if (isLocalEcho) { + Timber.v("Adding local echo reaction $reaction") + sum.sourceLocalEcho.add(txId) } else { - sum.count += 1 - if (isLocalEcho) { - 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) + Timber.v("Adding synced reaction $reaction") + sum.sourceEvents.add(reactionEventId) } + sum.addedByMe = sum.addedByMe || (userId == event.sender) } - } + } } + + } else { + Timber.e("Unknwon relation type ${content.relatesTo?.type} for event ${event.eventId}") } + } /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt index 0b29064c87..a04ed2160a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -46,11 +46,11 @@ internal class EventRelationsAggregationUpdater(monarchy: Monarchy, override fun processChanges(inserted: List, updated: List, deleted: List) { Timber.v("EventRelationsAggregationUpdater called with ${inserted.size} insertions") - val inserted = inserted - .mapNotNull { it.asDomain() to it.sendState } + val domainInserted = inserted + .map { it.asDomain() to it.sendState } val params = EventRelationsAggregationTask.Params( - inserted, + domainInserted, credentials.userId ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 6d6e4763d2..264909e956 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -15,15 +15,19 @@ */ package im.vector.matrix.android.internal.session.room.relation +import androidx.lifecycle.LiveData import androidx.work.OneTimeWorkRequest import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback 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.relation.RelationService import im.vector.matrix.android.api.util.Cancelable 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.EventAnnotationsSummaryEntity 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.where @@ -169,6 +173,18 @@ internal class DefaultRelationService(private val roomId: String, return CancelableWork(workRequest.id) } + + override fun getEventSummaryLive(eventId: String): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm -> + EventAnnotationsSummaryEntity.where(realm, eventId) + }, + { + it.asDomain() + } + ) + } + /** * 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. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt index 7823c076d4..986fe81bee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt @@ -38,7 +38,12 @@ internal class TaskExecutor(private val coroutineDispatchers: MatrixCoroutineDis 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) } diff --git a/vector/build.gradle b/vector/build.gradle index 7bcfdf72ac..9ee7c5e123 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -84,6 +84,7 @@ android { debug { resValue "bool", "debug_mode", "true" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" + buildConfigField "boolean", "SHOW_HIDDEN_TIMELINE_EVENTS", "false" signingConfig signingConfigs.debug } @@ -91,6 +92,7 @@ android { release { resValue "bool", "debug_mode", "false" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" + buildConfigField "boolean", "SHOW_HIDDEN_TIMELINE_EVENTS", "false" minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' diff --git a/vector/sampledata/reactions.json b/vector/sampledata/reactions.json new file mode 100644 index 0000000000..e2c8e4f4cd --- /dev/null +++ b/vector/sampledata/reactions.json @@ -0,0 +1,22 @@ +{ + "data": [ + { + "reaction" : "👍" + }, + { + "reaction" : "😀" + }, + { + "reaction" : "😞" + }, + { + "reaction" : "Not a reaction" + }, + { + "reaction" : "✅" + }, + { + "reaction" : "🎉" + } + ] +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/EmojiCompatFontProvider.kt b/vector/src/main/java/im/vector/riotredesign/EmojiCompatFontProvider.kt new file mode 100644 index 0000000000..a928d48b5f --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/EmojiCompatFontProvider.kt @@ -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() + + 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?) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt b/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt index 80a9ea8173..89ccfd9f37 100644 --- a/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt @@ -18,6 +18,10 @@ package im.vector.riotredesign import android.app.Application 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 androidx.multidex.MultiDex import com.airbnb.epoxy.EpoxyAsyncUtil @@ -43,6 +47,9 @@ import timber.log.Timber class VectorApplication : Application() { lateinit var appContext: Context + //font thread handler + private var mFontThreadHandler: Handler? = null + val vectorConfiguration: VectorConfiguration by inject() override fun onCreate() { @@ -66,9 +73,20 @@ class VectorApplication : Application() { val appModule = AppModule(applicationContext).definition val homeModule = HomeModule().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) registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks()) + + val fontRequest = FontRequest( + "com.google.android.gms.fonts", + "com.google.android.gms", + "Noto Color Emoji Compat", + R.array.com_google_android_gms_fonts_certs + ) + +// val efp = koin.koinContext.get() + FontsContractCompat.requestFont(this, fontRequest, koin.koinContext.get(), getFontThreadHandler()) + vectorConfiguration.initConfiguration() } @@ -82,4 +100,13 @@ class VectorApplication : Application() { vectorConfiguration.onConfigurationChanged(newConfig) } + private fun getFontThreadHandler(): Handler { + if (mFontThreadHandler == null) { + val handlerThread = HandlerThread("fonts") + handlerThread.start() + mFontThreadHandler = Handler(handlerThread.looper) + } + return mFontThreadHandler!! + } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt index 68db4f68d5..fb7ad75ee6 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt @@ -18,8 +18,8 @@ package im.vector.riotredesign.core.di import android.content.Context import android.content.Context.MODE_PRIVATE -import androidx.fragment.app.Fragment import im.vector.matrix.android.api.Matrix +import im.vector.riotredesign.EmojiCompatFontProvider import im.vector.riotredesign.core.error.ErrorFormatter import im.vector.riotredesign.core.resources.LocaleProvider import im.vector.riotredesign.core.resources.StringArrayProvider @@ -91,8 +91,12 @@ class AppModule(private val context: Context) { IncomingVerificationRequestHandler(context, get()) } - factory { (fragment: Fragment) -> - DefaultNavigator(fragment) as Navigator + factory { + DefaultNavigator() as Navigator + } + + single { + EmojiCompatFontProvider() } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/DimensionUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/DimensionUtils.kt index ab3654a33e..44659da6af 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/utils/DimensionUtils.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/DimensionUtils.kt @@ -28,4 +28,12 @@ object DimensionUtils { context.resources.displayMetrics ).toInt() } + + fun spToPx(sp: Int, context: Context): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + sp.toFloat(), + context.resources.displayMetrics + ).toInt() + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/Emoji.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/Emoji.kt new file mode 100644 index 0000000000..3595ad876e --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/Emoji.kt @@ -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 +} + diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/TextUtils.kt new file mode 100644 index 0000000000..558275565a --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/TextUtils.kt @@ -0,0 +1,29 @@ +package im.vector.riotredesign.core.utils + +import java.util.* + +object TextUtils { + + private val suffixes = TreeMap().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() + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt index ff08720d44..6619588157 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt @@ -54,12 +54,12 @@ class HomeDrawerFragment : VectorBaseFragment() { } } homeDrawerHeaderSettingsView.setOnClickListener { - navigator.openSettings() + navigator.openSettings(requireActivity()) } // Debug menu homeDrawerHeaderDebugView.setOnClickListener { - navigator.openDebug() + navigator.openDebug(requireActivity()) } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 09589a6d4b..22d527879b 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -69,7 +69,9 @@ class HomeModule { val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get()) val noticeEventFormatter = get(parameters = { parametersOf(fragment) }) val timelineMediaSizeProvider = TimelineMediaSizeProvider() - val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer, get()) + val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, + timelineDateFormatter, eventHtmlRenderer, get(), get()) + val timelineItemFactory = TimelineItemFactory( messageItemFactory = messageItemFactory, noticeItemFactory = NoticeItemFactory(noticeEventFormatter), diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt index 8d29c0ab8e..e46fd7f59e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt @@ -48,7 +48,7 @@ class HomeNavigator { activity?.let { //TODO enable eventId permalink. It doesn't work enough at the moment. it.drawerLayout?.closeDrawer(GravityCompat.START) - navigator.openRoom(roomId) + navigator.openRoom(roomId, it) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 4653d75166..552843258e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -53,6 +53,7 @@ import com.jaiselrahman.filepicker.model.MediaFile import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback 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.events.model.toModel 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.MessageActionsBottomSheet 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.item.MessageInformationData import im.vector.riotredesign.features.html.PillImageSpan @@ -235,11 +237,13 @@ class RoomDetailFragment : var formattedBody: CharSequence? = null if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { 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()) .usePlugin(HtmlPlugin.create()).build().render(document) } - composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody + composerLayout.composerRelatedMessageContent.text = formattedBody + ?: nonFormattedBody if (mode == SendMode.EDIT) { @@ -559,11 +563,11 @@ class RoomDetailFragment : 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) 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?) { editAggregatedSummary?.also { roomDetailViewModel.process(RoomDetailActions.ShowEditHistoryAction(informationData.eventId, it)) @@ -613,12 +622,17 @@ class RoomDetailFragment : val eventId = actionData.data?.toString() ?: return 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 -> { //I need info about the current selected message :/ copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false) - val snack = Snackbar.make(view!!, requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) - snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color)) - snack.show() + val msg = requireContext().getString(R.string.copied_to_clipboard) + showSnackWithMessage(msg, Snackbar.LENGTH_SHORT) } MessageMenuViewModel.ACTION_DELETE -> { val eventId = actionData.data?.toString() ?: return @@ -685,6 +699,13 @@ class RoomDetailFragment : val eventId = actionData.data.toString() 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 -> { Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index b02a71e8da..476ff99b86 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -55,7 +55,12 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, private val roomId = initialState.roomId private val eventId = initialState.eventId private val displayedEventsObservable = BehaviorRelay.create() - 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 { @@ -195,7 +200,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, } } SendMode.EDIT -> { - room.editTextMessage(state.selectedEvent?.root?.eventId ?: "", action.text, action.autoMarkdown) + room.editTextMessage(state.selectedEvent?.root?.eventId + ?: "", action.text, action.autoMarkdown) setState { copy( sendMode = SendMode.REGULAR, @@ -330,7 +336,6 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, room.updateQuickReaction(action.selectedReaction, action.opposite, action.targetEventId, session.sessionParams.credentials.userId) } - private fun handleSendMedia(action: RoomDetailActions.SendMedia) { val attachments = action.mediaFiles.map { ContentAttachmentData( @@ -350,6 +355,12 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { 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) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 9777970ecb..5b325c4ae9 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -46,22 +46,29 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler() ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { - interface Callback : ReactionPillCallback { + interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback { fun onEventVisible(event: TimelineEvent) fun onUrlClicked(url: String) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) fun onFileMessageClicked(messageFileContent: MessageFileContent) 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?) } interface ReactionPillCallback { 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() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index 44a753afbf..9c9bbc2db3 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -17,13 +17,13 @@ package im.vector.riotredesign.features.home.room.detail.timeline.action import android.app.Dialog import android.os.Bundle -import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProviders import butterknife.BindView 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.BottomSheetDialog 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.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 @@ -74,7 +73,7 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() { val cfm = childFragmentManager var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment 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() .replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment") .commit() @@ -89,7 +88,7 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() { var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment 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() .replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction") .commit() @@ -117,36 +116,26 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() { } override fun invalidate() = withState(viewModel) { - senderNameTextView.text = it.senderName - messageBodyTextView.text = it.messageBody - messageTimestampText.text = it.ts - - GlideApp.with(this).clear(senderAvatarImageView) - if (it.senderAvatarPath != null) { - GlideApp.with(this) - .load(it.senderAvatarPath) - .circleCrop() - .placeholder(AvatarRenderer.getPlaceholderDrawable(requireContext(), it.userId, it.senderName)) - .into(senderAvatarImageView) + if (it.showPreview) { + bottom_sheet_message_preview.isVisible = true + senderNameTextView.text = it.senderName + messageBodyTextView.text = it.messageBody + messageTimestampText.text = it.ts + AvatarRenderer.render(it.senderAvatarPath, it.userId, it.senderName, senderAvatarImageView) } 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 } - @Parcelize - data class ParcelableArgs( - val eventId: String, - val roomId: String, - val informationData: MessageInformationData - ) : Parcelable - companion object { fun newInstance(roomId: String, informationData: MessageInformationData): MessageActionsBottomSheet { return MessageActionsBottomSheet().apply { setArguments( - ParcelableArgs( + TimelineEventFragmentArgs( informationData.eventId, roomId, informationData diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 315927e30c..998ddf8195 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -15,18 +15,21 @@ */ package im.vector.riotredesign.features.home.room.detail.timeline.action +import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext 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.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.MessageType 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.renderer.html.HtmlRenderer import org.koin.android.ext.android.get +import org.koin.core.parameter.parametersOf import ru.noties.markwon.Markwon import ru.noties.markwon.html.HtmlPlugin import timber.log.Timber @@ -35,10 +38,12 @@ import java.util.* data class MessageActionState( - val userId: String, - val senderName: String, - val messageBody: CharSequence, - val ts: String?, + val userId: String = "", + val senderName: String = "", + val messageBody: CharSequence? = null, + val ts: String? = null, + val showPreview: Boolean = false, + val canReact: Boolean = false, val senderAvatarPath: String? = null) : MvRxState @@ -51,30 +56,47 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode override fun initialState(viewModelContext: ViewModelContext): MessageActionState? { val currentSession = viewModelContext.activity.get() - val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs + val fragment = (viewModelContext as? FragmentViewModelContext)?.fragment + val noticeFormatter = fragment?.get(parameters = { parametersOf(fragment) }) + val parcel = viewModelContext.args as TimelineEventFragmentArgs val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault()) val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId) + var body: CharSequence? = null + val originTs = event?.root?.originServerTs return if (event != null) { - val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() - ?: event.root.content.toModel() - val originTs = event.root.originServerTs - var body: CharSequence = messageContent?.body ?: "" - if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { - val parser = Parser.builder().build() - val document = parser.parse(messageContent.formattedBody?.trim() ?: messageContent.body) - // val renderer = HtmlRenderer.builder().build() - body = Markwon.builder(viewModelContext.activity) - .usePlugin(HtmlPlugin.create()).build().render(document) -// body = renderer.render(document) + when (event.root.type) { + EventType.MESSAGE -> { + val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() + ?: event.root.content.toModel() + body = messageContent?.body + if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { + val parser = Parser.builder().build() + val document = parser.parse(messageContent.formattedBody + ?: messageContent.body) + body = Markwon.builder(viewModelContext.activity) + .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( - event.root.sender ?: "", - parcel.informationData.memberName.toString(), - body, - dateFormat.format(Date(originTs ?: 0)), - currentSession.contentUrlResolver().resolveFullSize(parcel.informationData.avatarUrl) + userId = event.root.sender ?: "", + senderName = parcel.informationData.memberName?.toString() ?: "", + messageBody = body, + ts = dateFormat.format(Date(originTs ?: 0)), + showPreview = body != null, + canReact = event.root.type == EventType.MESSAGE && event.sendState.isSent(), + senderAvatarPath = parcel.informationData.avatarUrl ) } else { //can this happen? diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuFragment.kt index 3009f5fc67..2b47eae327 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuFragment.kt @@ -101,7 +101,7 @@ class MessageMenuFragment : BaseMvRxFragment() { companion object { - fun newInstance(pa: MessageActionsBottomSheet.ParcelableArgs): MessageMenuFragment { + fun newInstance(pa: TimelineEventFragmentArgs): MessageMenuFragment { val args = Bundle() args.putParcelable(MvRx.KEY_ARG, pa) val fragment = MessageMenuFragment() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index d80bd40276..f74a953e91 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -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 MessageMenuState(val actions: List) : MvRxState +data class MessageMenuState(val actions: List = emptyList()) : MvRxState /** * Manages list actions for a given message (copy / paste / forward...) @@ -46,27 +46,26 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel() - val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs + val parcel = viewModelContext.args as TimelineEventFragmentArgs val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId) ?: return null - val messageContent: MessageContent = event.annotations?.editSummary?.aggregatedContent?.toModel() - ?: event.root.content.toModel() ?: return null - val type = messageContent.type + val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() + ?: event.root.content.toModel() + val type = messageContent?.type - if (event.sendState == SendState.UNSENT) { + if (!event.sendState.isSent()) { //Resend and Delete return MessageMenuState( + //TODO listOf( - SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_send, event.root.eventId), - //TODO delete icon - SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId) +// SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_send, event.root.eventId), +// //TODO delete icon +// 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().apply { if (event.sendState == SendState.SENDING) { @@ -75,10 +74,12 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel() return event.root.sender == myUserId && ( @@ -182,7 +197,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel() + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.adapter_item_action_quick_reaction, container, false) ButterKnife.bind(this, view) @@ -68,6 +73,10 @@ class QuickReactionFragment : BaseMvRxFragment() { quickReact3Text.text = QuickReactionViewModel.likePositive quickReact4Text.text = QuickReactionViewModel.likeNegative + listOf(quickReact1Text, quickReact2Text, quickReact3Text, quickReact4Text).forEach { + it.typeface = fontProvider.typeface ?: Typeface.DEFAULT + } + //configure click listeners quickReact1Text.setOnClickListener { viewModel.toggleAgree(true) @@ -88,11 +97,11 @@ class QuickReactionFragment : BaseMvRxFragment() { TransitionManager.beginDelayedTransition(rootLayout) when (it.agreeTrigleState) { - TriggleState.NONE -> { + TriggleState.NONE -> { quickReact1Text.alpha = 1f quickReact2Text.alpha = 1f } - TriggleState.FIRST -> { + TriggleState.FIRST -> { quickReact1Text.alpha = 1f quickReact2Text.alpha = 0.2f @@ -103,11 +112,11 @@ class QuickReactionFragment : BaseMvRxFragment() { } } when (it.likeTriggleState) { - TriggleState.NONE -> { + TriggleState.NONE -> { quickReact3Text.alpha = 1f quickReact4Text.alpha = 1f } - TriggleState.FIRST -> { + TriggleState.FIRST -> { quickReact3Text.alpha = 1f quickReact4Text.alpha = 0.2f @@ -130,7 +139,7 @@ class QuickReactionFragment : BaseMvRxFragment() { } companion object { - fun newInstance(pa: MessageActionsBottomSheet.ParcelableArgs): QuickReactionFragment { + fun newInstance(pa: TimelineEventFragmentArgs): QuickReactionFragment { val args = Bundle() args.putParcelable(MvRx.KEY_ARG, pa) val fragment = QuickReactionFragment() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt index 36a07bee59..5252e51bda 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt @@ -32,15 +32,14 @@ enum class TriggleState { } data class QuickReactionState( - val agreeTrigleState: TriggleState, - val likeTriggleState: TriggleState, + val agreeTrigleState: TriggleState = TriggleState.NONE, + val likeTriggleState: TriggleState = TriggleState.NONE, /** Pair of 'clickedOn' and current toggles state*/ val selectionResult: Pair>? = null, - val eventId: String) : MvRxState + val eventId: String = "") : MvRxState /** * Quick reaction view model - * TODO: configure initial state from event */ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel(initialState) { @@ -88,15 +87,15 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel private fun getReactions(state: QuickReactionState, newState1: TriggleState?, newState2: TriggleState?): List { return ArrayList(4).apply { when (newState2 ?: state.likeTriggleState) { - TriggleState.FIRST -> add(likePositive) + TriggleState.FIRST -> add(likePositive) TriggleState.SECOND -> add(likeNegative) - else -> { + else -> { } } when (newState1 ?: state.agreeTrigleState) { - TriggleState.FIRST -> add(agreePositive) + TriggleState.FIRST -> add(agreePositive) TriggleState.SECOND -> add(agreeNegative) - else -> { + else -> { } } } @@ -114,9 +113,9 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel return when (reaction) { agreePositive -> agreeNegative agreeNegative -> agreePositive - likePositive -> likeNegative - likeNegative -> likePositive - else -> null + likePositive -> likeNegative + likeNegative -> likePositive + else -> null } } @@ -124,7 +123,7 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel // Args are accessible from the context. // val foo = vieWModelContext.args.foo val currentSession = viewModelContext.activity.get() - val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs + val parcel = viewModelContext.args as TimelineEventFragmentArgs val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId) ?: return null var agreeTriggle: TriggleState = TriggleState.NONE diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt new file mode 100644 index 0000000000..647af4b0f8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt @@ -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() { + + @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(R.id.itemSimpleReactionInfoKey) + val displayNameView by bind(R.id.itemSimpleReactionInfoMemberName) + val timeStampView by bind(R.id.itemSimpleReactionInfoTime) + } + +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt new file mode 100644 index 0000000000..5764563812 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt @@ -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 \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt new file mode 100644 index 0000000000..308c68bf9d --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt @@ -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() + + @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 } + + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionViewModel.kt new file mode 100644 index 0000000000..8854886512 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionViewModel.kt @@ -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> = 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(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() + 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 { + + 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() + val eventId = (viewModelContext.args as TimelineEventFragmentArgs).eventId + val lifecycleOwner = (viewModelContext as FragmentViewModelContext).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 + } + + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt new file mode 100644 index 0000000000..461fec2b13 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt @@ -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() { + + 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) + } + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt deleted file mode 100644 index d604bcd09c..0000000000 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotredesign.features.home.room.detail.timeline.factory - -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.call.CallInviteContent -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.riotredesign.R -import im.vector.riotredesign.core.resources.StringProvider -import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem -import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ - -class CallItemFactory(private val stringProvider: StringProvider) { - - fun create(event: TimelineEvent): NoticeItem? { - val text = buildNoticeText(event.root, event.senderName) ?: return null - return NoticeItem_() - .noticeText(text) - .avatarUrl(event.senderAvatar) - .memberName(event.senderName) - } - - private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { - return when { - EventType.CALL_INVITE == event.getClearType() -> { - val content = event.content.toModel() ?: return null - val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO - return if (isVideoCall) { - stringProvider.getString(R.string.notice_placed_video_call, senderName) - } else { - stringProvider.getString(R.string.notice_placed_voice_call, senderName) - } - } - EventType.CALL_ANSWER == event.getClearType() -> stringProvider.getString(R.string.notice_answered_call, senderName) - EventType.CALL_HANGUP == event.getClearType() -> stringProvider.getString(R.string.notice_ended_call, senderName) - else -> null - } - - } - - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 309eb0458b..4e89218356 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -26,6 +26,9 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.resources.StringProvider +import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar +import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ // This class handles timeline event who haven't been successfully decrypted @@ -34,24 +37,31 @@ class EncryptedItemFactory(private val stringProvider: StringProvider) { fun create(timelineEvent: TimelineEvent): VectorEpoxyModel<*>? { return when { EventType.ENCRYPTED == timelineEvent.root.getClearType() -> { - val cryptoError = timelineEvent.root.mCryptoError - val errorDescription = - if (cryptoError?.code == MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE) { - stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id) - } else { - cryptoError?.message - } + val cryptoError = timelineEvent.root.mCryptoError + val errorDescription = + if (cryptoError?.code == MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE) { + stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id) + } else { + cryptoError?.message + } - val message = stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) - val spannableStr = SpannableString(message) - spannableStr.setSpan(StyleSpan(Typeface.ITALIC), 0, message.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - // TODO This is not correct format for error, change it - return NoticeItem_() - .noticeText(spannableStr) - .avatarUrl(timelineEvent.senderAvatar) - .memberName(timelineEvent.senderName) + val message = stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) + val spannableStr = SpannableString(message) + spannableStr.setSpan(StyleSpan(Typeface.ITALIC), 0, message.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + // TODO This is not correct format for error, change it + val informationData = MessageInformationData( + eventId = timelineEvent.root.eventId ?: "?", + senderId = timelineEvent.root.sender ?: "", + sendState = timelineEvent.sendState, + avatarUrl = timelineEvent.senderAvatar(), + memberName = timelineEvent.senderName(), + showInformation = false + ) + return NoticeItem_() + .noticeText(spannableStr) + .informationData(informationData) } - else -> null + else -> null } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 11a86850de..d0ca0ffcfa 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -23,6 +23,9 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent import im.vector.riotredesign.R import im.vector.riotredesign.core.resources.StringProvider +import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar +import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ @@ -30,10 +33,17 @@ class EncryptionItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { val text = buildNoticeText(event.root, event.senderName) ?: return null + val informationData = MessageInformationData( + eventId = event.root.eventId ?: "?", + senderId = event.root.sender ?: "", + sendState = event.sendState, + avatarUrl = event.senderAvatar(), + memberName = event.senderName(), + showInformation = false + ) return NoticeItem_() .noticeText(text) - .avatarUrl(event.senderAvatar) - .memberName(event.senderName) + .informationData(informationData) } private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 8646da46dc..a4b3125147 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -23,7 +23,6 @@ import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.text.style.RelativeSizeSpan import android.view.View -import androidx.annotation.ColorRes import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan 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.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotredesign.EmojiCompatFontProvider import im.vector.riotredesign.R import im.vector.riotredesign.core.epoxy.VectorEpoxyModel 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.StringProvider 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.room.detail.timeline.TimelineEventController 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 timelineDateFormatter: TimelineDateFormatter, private val htmlRenderer: EventHtmlRenderer, - private val stringProvider: StringProvider) { + private val stringProvider: StringProvider, + private val emojiCompatFontProvider: EmojiCompatFontProvider) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?, @@ -115,24 +115,24 @@ class MessageItemFactory(private val colorProvider: ColorProvider, // val all = event.root.toContent() // val ev = all.toModel() return when (messageContent) { - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, + is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, hasBeenEdited, event.annotations?.editSummary, callback) - is MessageTextContent -> buildTextMessageItem(event.sendState, + is MessageTextContent -> buildTextMessageItem(event.sendState, messageContent, informationData, hasBeenEdited, event.annotations?.editSummary, callback ) - is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback) + is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback) - is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback) - is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback) - is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback) - else -> buildNotHandledMessageItem(messageContent) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback) + is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback) + is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback) + else -> buildNotHandledMessageItem(messageContent) } } @@ -141,23 +141,17 @@ class MessageItemFactory(private val colorProvider: ColorProvider, callback: TimelineEventController.Callback?): MessageFileItem? { return MessageFileItem_() .informationData(informationData) + .avatarCallback(callback) .filename(messageContent.body) .iconRes(R.drawable.filetype_audio) .reactionPillCallback(callback) - .avatarClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onAvatarClicked(informationData) - })) - .memberClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onMemberNameClicked(informationData) - })) + .emojiTypeFace(emojiCompatFontProvider.typeface) .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> + DebouncedClickListener(View.OnClickListener { view: View -> callback?.onEventCellClicked(informationData, messageContent, view) })) .clickListener( - DebouncedClickListener(View.OnClickListener { _ -> + DebouncedClickListener(View.OnClickListener { callback?.onAudioMessageClicked(messageContent) })) .longClickListener { view -> @@ -171,17 +165,11 @@ class MessageItemFactory(private val colorProvider: ColorProvider, callback: TimelineEventController.Callback?): MessageFileItem? { return MessageFileItem_() .informationData(informationData) + .avatarCallback(callback) .filename(messageContent.body) .reactionPillCallback(callback) + .emojiTypeFace(emojiCompatFontProvider.typeface) .iconRes(R.drawable.filetype_attachment) - .avatarClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onAvatarClicked(informationData) - })) - .memberClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onMemberNameClicked(informationData) - })) .cellClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onEventCellClicked(informationData, messageContent, view) @@ -219,16 +207,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageImageVideoItem_() .playable(messageContent.info?.mimeType == "image/gif") .informationData(informationData) + .avatarCallback(callback) .mediaData(data) .reactionPillCallback(callback) - .avatarClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onAvatarClicked(informationData) - })) - .memberClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onMemberNameClicked(informationData) - })) + .emojiTypeFace(emojiCompatFontProvider.typeface) .clickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onImageMessageClicked(messageContent, data, view) @@ -266,16 +248,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageImageVideoItem_() .playable(true) .informationData(informationData) + .avatarCallback(callback) .mediaData(thumbnailData) .reactionPillCallback(callback) - .avatarClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onAvatarClicked(informationData) - })) - .memberClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onMemberNameClicked(informationData) - })) + .emojiTypeFace(emojiCompatFontProvider.typeface) .cellClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onEventCellClicked(informationData, messageContent, view) @@ -310,15 +286,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider, } } .informationData(informationData) + .avatarCallback(callback) .reactionPillCallback(callback) - .avatarClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onAvatarClicked(informationData) - })) - .memberClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onMemberNameClicked(informationData) - })) + .emojiTypeFace(emojiCompatFontProvider.typeface) + //click on the text .cellClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onEventCellClicked(informationData, messageContent, view) @@ -378,11 +349,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageTextItem_() .message(message) .informationData(informationData) + .avatarCallback(callback) .reactionPillCallback(callback) - .avatarClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onAvatarClicked(informationData) - })) + .emojiTypeFace(emojiCompatFontProvider.typeface) .memberClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onMemberNameClicked(informationData) @@ -417,15 +386,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider, } } .informationData(informationData) + .avatarCallback(callback) .reactionPillCallback(callback) - .avatarClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onAvatarClicked(informationData) - })) - .memberClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onMemberNameClicked(informationData) - })) + .emojiTypeFace(emojiCompatFontProvider.typeface) .cellClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onEventCellClicked(informationData, messageContent, view) @@ -440,14 +403,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, callback: TimelineEventController.Callback?): RedactedMessageItem? { return RedactedMessageItem_() .informationData(informationData) - .avatarClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onAvatarClicked(informationData) - })) - .memberClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onMemberNameClicked(informationData) - })) + .avatarCallback(callback) } private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index fc756c9c12..dabebd2807 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -17,23 +17,32 @@ package im.vector.riotredesign.features.home.room.detail.timeline.factory 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.helper.senderAvatar import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ 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 senderName = event.senderName() - val senderAvatar = event.senderAvatar() + val informationData = MessageInformationData( + eventId = event.root.eventId ?: "?", + senderId = event.root.sender ?: "", + sendState = event.sendState, + avatarUrl = event.senderAvatar(), + memberName = event.senderName(), + showInformation = false + ) return NoticeItem_() .noticeText(formattedText) - .avatarUrl(senderAvatar) - .memberName(senderName) + .informationData(informationData) + .baseCallback(callback) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt deleted file mode 100644 index c898103eac..0000000000 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotredesign.features.home.room.detail.timeline.factory - -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility -import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent -import im.vector.matrix.android.api.session.room.model.RoomMember -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.riotredesign.R -import im.vector.riotredesign.core.resources.StringProvider -import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem -import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ - - -class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvider) { - - fun create(event: TimelineEvent): NoticeItem? { - val noticeText = buildNoticeText(event.root, event.senderName) ?: return null - return NoticeItem_() - .noticeText(noticeText) - .avatarUrl(event.senderAvatar) - .memberName(event.senderName) - } - - private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { - val historyVisibility = event.content.toModel()?.historyVisibility - ?: return null - val formattedVisibility = when (historyVisibility) { - RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) - RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited) - RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) - RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) - } - return stringProvider.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility) - } - - -} - - diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index df8d13d038..c4609f88ce 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -17,10 +17,16 @@ 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.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.riotredesign.core.epoxy.EmptyItem_ 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.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 class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, @@ -43,7 +49,7 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, EventType.STATE_HISTORY_VISIBILITY, EventType.CALL_INVITE, EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> noticeItemFactory.create(event) + EventType.CALL_ANSWER -> noticeItemFactory.create(event, callback) // Crypto EventType.ENCRYPTION -> encryptionItemFactory.create(event) @@ -53,9 +59,32 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STICKER, EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event) + else -> { - Timber.w("Ignored event (type: ${event.root.type}") - null + //These are just for debug to display hidden event, they should be filtered out in normal mode + 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() + ?: 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) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index e5d9ade0ec..858a8abc1a 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -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.message.MessageContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotredesign.BuildConfig import im.vector.riotredesign.core.extensions.localDateTime 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( EventType.MESSAGE, EventType.STATE_ROOM_NAME, @@ -41,6 +45,11 @@ object TimelineDisplayableEvents { EventType.STICKER, EventType.STATE_ROOM_CREATE ) + + val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf( + EventType.REDACTION, + EventType.REACTION + ) } fun TimelineEvent.isDisplayable(): Boolean { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt index 0ce01a88fc..7749e152b3 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -16,6 +16,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item +import android.graphics.Typeface import android.os.Build import android.view.View import android.view.ViewGroup @@ -28,6 +29,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import im.vector.riotredesign.R +import im.vector.riotredesign.core.utils.DebouncedClickListener import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController @@ -44,15 +46,26 @@ abstract class AbsMessageItem : BaseEventItem() { @EpoxyAttribute var cellClickListener: View.OnClickListener? = null - @EpoxyAttribute - var avatarClickListener: View.OnClickListener? = null - @EpoxyAttribute var memberClickListener: View.OnClickListener? = null + @EpoxyAttribute + var emojiTypeFace: Typeface? = null + @EpoxyAttribute 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 { override fun onReacted(reactionButton: ReactionButton) { reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, true) @@ -61,6 +74,10 @@ abstract class AbsMessageItem : BaseEventItem() { override fun onUnReacted(reactionButton: ReactionButton) { reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, false) } + + override fun onLongClick(reactionButton: ReactionButton) { + reactionPillCallback?.onLongClickOnReactionPill(informationData, reactionButton.reactionString) + } } override fun bind(holder: H) { @@ -73,9 +90,9 @@ abstract class AbsMessageItem : BaseEventItem() { width = size } holder.avatarImageView.visibility = View.VISIBLE - holder.avatarImageView.setOnClickListener(avatarClickListener) + holder.avatarImageView.setOnClickListener(_avatarClickListener) holder.memberNameView.visibility = View.VISIBLE - holder.memberNameView.setOnClickListener(memberClickListener) + holder.memberNameView.setOnClickListener(_memberNameClickListener) holder.timeView.visibility = View.VISIBLE holder.timeView.text = informationData.time holder.memberNameView.text = informationData.memberName @@ -108,7 +125,7 @@ abstract class AbsMessageItem : BaseEventItem() { //clear all reaction buttons (but not the Flow helper!) holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true } val idToRefInFlow = ArrayList() - 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 -> reactionButton.isVisible = true reactionButton.reactedListener = reactionClickListener @@ -116,6 +133,7 @@ abstract class AbsMessageItem : BaseEventItem() { idToRefInFlow.add(reactionButton.id) reactionButton.reactionString = reaction.key reactionButton.reactionCount = reaction.count + reactionButton.emojiTypeFace = emojiTypeFace reactionButton.setChecked(reaction.addedByMe) reactionButton.isEnabled = reaction.synced } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt index 7fa0e48294..128152417c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -23,6 +23,7 @@ import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotredesign.R +import im.vector.riotredesign.core.utils.containsOnlyEmojis import im.vector.riotredesign.features.html.PillImageSpan import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -51,12 +52,20 @@ abstract class MessageTextItem : AbsMessageItem() { override fun bind(holder: Holder) { super.bind(holder) - 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 ?: "", TextViewCompat.getTextMetricsParams(holder.messageView), null) + holder.messageView.setTextFuture(textFuture) holder.messageView.renderSendState() holder.messageView.setOnClickListener(cellClickListener) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt index dcb0bdf4ad..d190875f20 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt @@ -23,27 +23,36 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotredesign.R 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) abstract class NoticeItem : BaseEventItem() { @EpoxyAttribute var noticeText: CharSequence? = null - @EpoxyAttribute - var avatarUrl: String? = null - @EpoxyAttribute - var userId: String = "" - @EpoxyAttribute - var memberName: CharSequence? = null - @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) { super.bind(holder) 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) } @@ -51,7 +60,6 @@ abstract class NoticeItem : BaseEventItem() { class Holder : BaseHolder() { override fun getStubId(): Int = STUB_ID - val avatarImageView by bind(R.id.itemNoticeAvatarView) val noticeTextView by bind(R.id.itemNoticeTextView) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt index 247ab070b5..e354be2f74 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt @@ -72,7 +72,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O setupRecyclerView() roomListViewModel.subscribe { renderState(it) } roomListViewModel.openRoomLiveData.observeEvent(this) { - navigator.openRoom(it) + navigator.openRoom(it, requireActivity()) } createChatFabMenu.listener = this @@ -116,7 +116,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O override fun openRoomDirectory() { - navigator.openRoomDirectory() + navigator.openRoomDirectory(requireActivity()) } override fun createDirectChat() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotredesign/features/navigation/DefaultNavigator.kt index 307d4fe812..44c1cfe366 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/navigation/DefaultNavigator.kt @@ -17,6 +17,7 @@ package im.vector.riotredesign.features.navigation import android.app.Activity +import android.content.Context import android.content.Intent import androidx.fragment.app.Fragment 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.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 intent = RoomDetailActivity.newIntent(activity, args) - activity.startActivity(intent) + val intent = RoomDetailActivity.newIntent(context, args) + context.startActivity(intent) } - override fun openRoomPreview(publicRoom: PublicRoom) { - val intent = RoomPreviewActivity.getIntent(activity, publicRoom) - activity.startActivity(intent) + override fun openRoomPreview(publicRoom: PublicRoom, context: Context) { + val intent = RoomPreviewActivity.getIntent(context, publicRoom) + context.startActivity(intent) } - override fun openRoomDirectory() { - val intent = Intent(activity, RoomDirectoryActivity::class.java) - activity.startActivity(intent) + override fun openRoomDirectory(context: Context) { + val intent = Intent(context, RoomDirectoryActivity::class.java) + context.startActivity(intent) } - override fun openSettings() { - val intent = VectorSettingsActivity.getIntent(activity, "TODO") - activity.startActivity(intent) + override fun openSettings(context: Context) { + val intent = VectorSettingsActivity.getIntent(context, "TODO") + context.startActivity(intent) } - override fun openDebug() { - activity.startActivity(Intent(activity, DebugMenuActivity::class.java)) + override fun openDebug(context: Context) { + context.startActivity(Intent(context, DebugMenuActivity::class.java)) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotredesign/features/navigation/Navigator.kt index 518e8de818..8873227b00 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/navigation/Navigator.kt @@ -16,18 +16,19 @@ package im.vector.riotredesign.features.navigation +import android.content.Context import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom 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) } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt index a75accd9ba..7ad57d5368 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt @@ -19,23 +19,20 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.graphics.Typeface -import android.os.Handler -import android.os.HandlerThread import android.util.TypedValue import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.widget.SearchView import androidx.appcompat.widget.Toolbar -import androidx.core.provider.FontRequest -import androidx.core.provider.FontsContractCompat import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import com.google.android.material.tabs.TabLayout +import im.vector.riotredesign.EmojiCompatFontProvider import im.vector.riotredesign.R import im.vector.riotredesign.core.platform.VectorBaseActivity 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: Move font request to app */ -class EmojiReactionPickerActivity : VectorBaseActivity() { +class EmojiReactionPickerActivity : VectorBaseActivity(), EmojiCompatFontProvider.FontProviderListener { + private lateinit var tabLayout: TabLayout lateinit var viewModel: EmojiChooserViewModel - private var mHandler: Handler? = null - override fun getMenuRes(): Int = R.menu.menu_emoji_reaction_picker override fun getLayoutRes(): Int = R.layout.activity_emoji_reaction_picker override fun getTitleRes(): Int = R.string.title_activity_emoji_reaction_picker + val emojiCompatFontProvider by inject() + private var tabLayoutSelectionListener = object : TabLayout.BaseOnTabSelectedListener { 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() { configureToolbar(emojiPickerToolbar) - requestEmojivUnicode10CompatibleFont() + emojiCompatFontProvider.let { + EmojiDrawView.configureTextPaint(this, it.typeface) + it.addListener(this) + } tabLayout = findViewById(R.id.tabs) @@ -124,27 +116,13 @@ class EmojiReactionPickerActivity : VectorBaseActivity() { }) } - private fun requestEmojivUnicode10CompatibleFont() { - val fontRequest = FontRequest( - "com.google.android.gms.fonts", - "com.google.android.gms", - "Noto Color Emoji Compat", - R.array.com_google_android_gms_fonts_certs - ) + override fun compatibilityFontUpdate(typeface: Typeface?) { + EmojiDrawView.configureTextPaint(this, typeface) + } - EmojiDrawView.configureTextPaint(this, null) - val callback = object : FontsContractCompat.FontRequestCallback() { - - 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 onDestroy() { + emojiCompatFontProvider.removeListener(this) + super.onDestroy() } override fun onCreateOptionsMenu(menu: Menu): Boolean { diff --git a/vector/src/main/java/im/vector/riotredesign/features/reactions/widget/ReactionButton.kt b/vector/src/main/java/im/vector/riotredesign/features/reactions/widget/ReactionButton.kt index c4a87d0fe7..8250b66854 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/reactions/widget/ReactionButton.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/reactions/widget/ReactionButton.kt @@ -21,10 +21,10 @@ import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.content.Context import android.content.res.TypedArray +import android.graphics.Typeface import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.LayoutInflater -import android.view.MotionEvent import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.DecelerateInterpolator @@ -36,13 +36,15 @@ import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.core.content.ContextCompat import im.vector.riotredesign.R +import im.vector.riotredesign.core.utils.TextUtils /** * An animated reaction button. * 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, - defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr), View.OnClickListener { + defStyleAttr: Int = 0) + : FrameLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener { companion object { private val DECCELERATE_INTERPOLATOR = DecelerateInterpolator() @@ -56,6 +58,11 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut private var reactionSelector: View? = null + var emojiTypeFace: Typeface? = null + set(value) { + field = value + emojiView?.typeface = value ?: Typeface.DEFAULT + } private var dotsView: DotsView private var circleView: CircleView @@ -68,7 +75,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut var reactionCount = 11 set(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) 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) @@ -128,6 +137,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut val status = array.getBoolean(R.styleable.ReactionButton_toggled, false) setChecked(status) setOnClickListener(this) + setOnLongClickListener(this) array.recycle() } @@ -234,40 +244,45 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut * @param event * @return */ - override fun onTouchEvent(event: MotionEvent): Boolean { - if (!isEnabled) - return true +// override fun onTouchEvent(event: MotionEvent): Boolean { +// if (!isEnabled) +// 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) { - 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 + override fun onLongClick(v: View?): Boolean { + reactedListener?.onLongClick(this) + return reactedListener != null } /** @@ -327,5 +342,6 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut interface ReactedListener { fun onReacted(reactionButton: ReactionButton) fun onUnReacted(reactionButton: ReactionButton) + fun onLongClick(reactionButton: ReactionButton) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsFragment.kt index aae438b7d6..5deab028f2 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/PublicRoomsFragment.kt @@ -124,12 +124,12 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback when (joinState) { JoinState.JOINED -> { - navigator.openRoom(publicRoom.roomId) + navigator.openRoom(publicRoom.roomId, requireActivity()) } JoinState.NOT_JOINED, JoinState.JOINING_ERROR -> { // ROOM PREVIEW - navigator.openRoomPreview(publicRoom) + navigator.openRoomPreview(publicRoom, requireActivity()) } else -> { Snackbar.make(publicRoomsCoordinator, getString(R.string.please_wait), Snackbar.LENGTH_SHORT) diff --git a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt index 074995557a..3f389a4609 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt @@ -108,7 +108,7 @@ class RoomPreviewNoPreviewFragment : VectorBaseFragment() { // Quit this screen requireActivity().finish() // Open room - navigator.openRoom(roomPreviewData.roomId) + navigator.openRoom(roomPreviewData.roomId, requireActivity()) } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt index 859b773616..de68ba6b1e 100755 --- a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt @@ -2322,7 +2322,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref } // add the edit text preference - preference = VectorPreference(activity!!).apply { + preference = VectorPreference(requireActivity()).apply { mTypeface = typeFaceHighlight } diff --git a/vector/src/main/res/drawable/ic_view_reactions.xml b/vector/src/main/res/drawable/ic_view_reactions.xml new file mode 100644 index 0000000000..f4106852b8 --- /dev/null +++ b/vector/src/main/res/drawable/ic_view_reactions.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/vector/src/main/res/drawable/rounded_rect_shape.xml b/vector/src/main/res/drawable/rounded_rect_shape.xml index cf083254f3..e9c0a27f59 100644 --- a/vector/src/main/res/drawable/rounded_rect_shape.xml +++ b/vector/src/main/res/drawable/rounded_rect_shape.xml @@ -2,7 +2,7 @@ - + diff --git a/vector/src/main/res/layout/activity_emoji_reaction_picker.xml b/vector/src/main/res/layout/activity_emoji_reaction_picker.xml index 38694933b3..475ff54c3a 100644 --- a/vector/src/main/res/layout/activity_emoji_reaction_picker.xml +++ b/vector/src/main/res/layout/activity_emoji_reaction_picker.xml @@ -15,25 +15,23 @@ tools:layout="@layout/emoji_chooser_fragment" /> + android:layout_height="wrap_content" + android:elevation="4dp"> + app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways" + tools:title="@string/reactions" /> + android:layout_height="40dp" /> diff --git a/vector/src/main/res/layout/bottom_sheet_display_reactions.xml b/vector/src/main/res/layout/bottom_sheet_display_reactions.xml new file mode 100644 index 0000000000..0f5b63654d --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_display_reactions.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_message_actions.xml b/vector/src/main/res/layout/bottom_sheet_message_actions.xml index 0de471bdd1..d1cb8c9fb9 100644 --- a/vector/src/main/res/layout/bottom_sheet_message_actions.xml +++ b/vector/src/main/res/layout/bottom_sheet_message_actions.xml @@ -86,7 +86,8 @@ tools:text="Friday 8pm" /> - @@ -94,18 +95,22 @@ + android:layout_height="wrap_content" + tools:background="@android:color/holo_green_light" + tools:layout_height="180dp" /> - - + android:layout_height="wrap_content" + tools:background="@android:color/holo_blue_dark" + tools:layout_height="250dp" /> diff --git a/vector/src/main/res/layout/item_simple_reaction_info.xml b/vector/src/main/res/layout/item_simple_reaction_info.xml new file mode 100644 index 0000000000..0458b17126 --- /dev/null +++ b/vector/src/main/res/layout/item_simple_reaction_info.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index 07b43f7bc1..8163b3acdd 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -4,6 +4,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:addStatesFromChildren="true" + android:background="?attr/selectableItemBackground" android:paddingLeft="8dp" android:paddingRight="8dp"> @@ -31,9 +32,9 @@ - + + + + + + tools:text="13450" /> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 3554d61948..8d39ffe774 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -20,6 +20,8 @@ Agree Like Add Reaction + View Reactions + Reactions Event deleted by user Event moderated by room admin diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index 7451f43fcb..a6780efd25 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -26,6 +26,10 @@ "sans-serif" + +