From adbfde94d67116912713fcf6ce4f47b423e1926f Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 4 Jun 2019 17:14:12 +0200 Subject: [PATCH 01/18] Fix / move read receipt on m.replace events --- .../features/home/room/detail/RoomDetailViewModel.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 b02a71e8..7a47cff0 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 @@ -350,6 +350,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) { From 53c91dc0c28a0f72623c74e347c014f071254129 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 4 Jun 2019 17:14:52 +0200 Subject: [PATCH 02/18] Ignore server aggregation until API ready --- .../room/EventRelationsAggregationTask.kt | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) 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 4ff661e2..a66b65ab 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 @@ -44,6 +44,9 @@ internal interface EventRelationsAggregationTask : Task { return monarchy.tryTransactionAsync { realm -> update(realm, params.events, params.userId) @@ -155,20 +158,22 @@ 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 + } } } } From d2f648edec84a0ff0424738395f7c42c39b08a19 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 4 Jun 2019 17:16:02 +0200 Subject: [PATCH 03/18] Use Font emoji compat for quickReactions and pills --- .../riotredesign/EmojiCompatFontProvider.kt | 48 +++++++++++++++++ .../vector/riotredesign/VectorApplication.kt | 29 +++++++++- .../vector/riotredesign/core/di/AppModule.kt | 5 ++ .../riotredesign/features/home/HomeModule.kt | 3 +- .../timeline/action/QuickReactionFragment.kt | 17 ++++-- .../timeline/factory/MessageItemFactory.kt | 33 +++++++----- .../detail/timeline/item/AbsMessageItem.kt | 5 ++ .../reactions/EmojiReactionPickerActivity.kt | 54 ++++++------------- .../reactions/widget/ReactionButton.kt | 14 +++-- 9 files changed, 149 insertions(+), 59 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotredesign/EmojiCompatFontProvider.kt 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 00000000..a928d48b --- /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 88cee674..e1d85fe3 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 @@ -41,6 +45,10 @@ import timber.log.Timber class VectorApplication : Application() { + //font thread handler + private var mFontThreadHandler: Handler? = null + + val vectorConfiguration: VectorConfiguration by inject() override fun onCreate() { @@ -63,10 +71,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) + 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() } @@ -81,4 +99,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 dc3068cf..542c0953 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 @@ -20,6 +20,7 @@ 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 @@ -90,5 +91,9 @@ class AppModule(private val context: Context) { DefaultNavigator(fragment) as Navigator } + single { + EmojiCompatFontProvider() + } + } } \ 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 d6297b6f..ce985abb 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 @@ -72,7 +72,8 @@ class HomeModule { val timelineMediaSizeProvider = TimelineMediaSizeProvider() val colorProvider = ColorProvider(fragment.requireContext()) val timelineDateFormatter = get() - val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer, get()) + val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, + timelineDateFormatter, eventHtmlRenderer, get(), get()) val timelineItemFactory = TimelineItemFactory( messageItemFactory = messageItemFactory, diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt index eac10a90..2d601544 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt @@ -15,6 +15,7 @@ */ package im.vector.riotredesign.features.home.room.detail.timeline.action +import android.graphics.Typeface import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -28,7 +29,9 @@ import com.airbnb.mvrx.BaseMvRxFragment 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 org.koin.android.ext.android.inject /** * Quick Reaction Fragment (agree / like reactions) @@ -54,6 +57,8 @@ class QuickReactionFragment : BaseMvRxFragment() { var interactionListener: InteractionListener? = null + val fontProvider by inject() + 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 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 d61848af..7571fead 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 @@ -33,6 +33,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 @@ -55,7 +56,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 +117,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) } } @@ -144,20 +146,21 @@ class MessageItemFactory(private val colorProvider: ColorProvider, .filename(messageContent.body) .iconRes(R.drawable.filetype_audio) .reactionPillCallback(callback) + .emojiTypeFace(emojiCompatFontProvider.typeface) .avatarClickListener( - DebouncedClickListener(View.OnClickListener { view -> + DebouncedClickListener(View.OnClickListener { callback?.onAvatarClicked(informationData) })) .memberClickListener( - DebouncedClickListener(View.OnClickListener { view -> + DebouncedClickListener(View.OnClickListener { callback?.onMemberNameClicked(informationData) })) .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 -> @@ -173,6 +176,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, .informationData(informationData) .filename(messageContent.body) .reactionPillCallback(callback) + .emojiTypeFace(emojiCompatFontProvider.typeface) .iconRes(R.drawable.filetype_attachment) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> @@ -221,6 +225,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, .informationData(informationData) .mediaData(data) .reactionPillCallback(callback) + .emojiTypeFace(emojiCompatFontProvider.typeface) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onAvatarClicked(informationData) @@ -268,6 +273,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, .informationData(informationData) .mediaData(thumbnailData) .reactionPillCallback(callback) + .emojiTypeFace(emojiCompatFontProvider.typeface) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onAvatarClicked(informationData) @@ -311,6 +317,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, } .informationData(informationData) .reactionPillCallback(callback) + .emojiTypeFace(emojiCompatFontProvider.typeface) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onAvatarClicked(informationData) @@ -384,6 +391,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, .message(message) .informationData(informationData) .reactionPillCallback(callback) + .emojiTypeFace(emojiCompatFontProvider.typeface) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onAvatarClicked(informationData) @@ -423,6 +431,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, } .informationData(informationData) .reactionPillCallback(callback) + .emojiTypeFace(emojiCompatFontProvider.typeface) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onAvatarClicked(informationData) 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 0ce01a88..b74f7bca 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 @@ -50,6 +51,9 @@ abstract class AbsMessageItem : BaseEventItem() { @EpoxyAttribute var memberClickListener: View.OnClickListener? = null + @EpoxyAttribute + var emojiTypeFace: Typeface? = null + @EpoxyAttribute var reactionPillCallback: TimelineEventController.ReactionPillCallback? = null @@ -116,6 +120,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/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt index a75accd9..7ad57d53 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 c4a87d0f..b0bf08e3 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,6 +21,7 @@ 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 @@ -56,6 +57,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 @@ -97,6 +103,8 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut countTextView?.text = reactionCount.toString() + emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT + val array = context.obtainStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr, 0) onDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape) @@ -239,7 +247,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut return true when (event.action) { - MotionEvent.ACTION_DOWN -> + 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. @@ -248,7 +256,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut */ isPressed = true - MotionEvent.ACTION_MOVE -> { + MotionEvent.ACTION_MOVE -> { val x = event.x val y = event.y val isInside = x > 0 && x < width && y > 0 && y < height @@ -257,7 +265,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut } } - MotionEvent.ACTION_UP -> { + 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) { From 440442bb99b45cfb248343f82bb221707ac15d4c Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 5 Jun 2019 19:23:57 +0200 Subject: [PATCH 04/18] New View Reactions bottom sheet + visible on reaction long click + Reaction pills size adapt to count, and number format --- .../room/model/relation/RelationService.kt | 3 + .../room/relation/DefaultRelationService.kt | 16 +++ vector/sampledata/reactions.json | 22 ++++ .../riotredesign/core/utils/TextUtils.kt | 29 +++++ .../home/room/detail/RoomDetailFragment.kt | 12 +- .../timeline/TimelineEventController.kt | 1 + .../action/MessageActionsBottomSheet.kt | 15 +-- .../action/MessageActionsViewModel.kt | 3 +- .../timeline/action/MessageMenuFragment.kt | 2 +- .../timeline/action/MessageMenuViewModel.kt | 2 +- .../timeline/action/QuickReactionFragment.kt | 2 +- .../timeline/action/QuickReactionViewModel.kt | 2 +- .../timeline/action/ReactionInfoSimpleItem.kt | 39 +++++++ .../action/TimelineEventFragmentArgs.kt | 12 ++ .../action/ViewReactionBottomSheet.kt | 71 ++++++++++++ .../timeline/action/ViewReactionViewModel.kt | 106 ++++++++++++++++++ .../action/ViewReactionsEpoxyController.kt | 19 ++++ .../detail/timeline/item/AbsMessageItem.kt | 6 +- .../reactions/widget/ReactionButton.kt | 80 +++++++------ .../layout/bottom_sheet_display_reactions.xml | 40 +++++++ .../res/layout/item_simple_reaction_info.xml | 45 ++++++++ .../src/main/res/layout/reaction_button.xml | 32 ++++-- vector/src/main/res/values/strings_riotX.xml | 1 + 23 files changed, 492 insertions(+), 68 deletions(-) create mode 100644 vector/sampledata/reactions.json create mode 100644 vector/src/main/java/im/vector/riotredesign/core/utils/TextUtils.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/TimelineEventFragmentArgs.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt create mode 100644 vector/src/main/res/layout/bottom_sheet_display_reactions.xml create mode 100644 vector/src/main/res/layout/item_simple_reaction_info.xml 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 bc924728..ac08b64f 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/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 6d6e4763..264909e9 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/vector/sampledata/reactions.json b/vector/sampledata/reactions.json new file mode 100644 index 00000000..e2c8e4f4 --- /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/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/TextUtils.kt new file mode 100644 index 00000000..55827556 --- /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/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 4653d751..5ab0e46c 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 @@ -84,6 +84,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 +236,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) { @@ -593,6 +596,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)) 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 84f97047..175702cf 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 @@ -62,6 +62,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, interface ReactionPillCallback { fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) + fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) } 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 44a753af..fa94043e 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,7 +17,6 @@ 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 @@ -36,7 +35,6 @@ 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 /** * Bottom sheet fragment that shows a message preview with list of contextual actions @@ -74,7 +72,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 +87,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() @@ -135,18 +133,11 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() { } - @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 9198f8ee..a307afe3 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 @@ -25,7 +25,6 @@ import im.vector.matrix.android.api.session.room.model.message.MessageTextConten import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.riotredesign.core.platform.VectorViewModel import org.commonmark.parser.Parser -import org.commonmark.renderer.html.HtmlRenderer import org.koin.android.ext.android.get import ru.noties.markwon.Markwon import ru.noties.markwon.html.HtmlPlugin @@ -51,7 +50,7 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode override fun initialState(viewModelContext: ViewModelContext): MessageActionState? { val currentSession = viewModelContext.activity.get() - val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs + val parcel = viewModelContext.args as TimelineEventFragmentArgs val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault()) 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 3009f5fc..2b47eae3 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 86ddf866..776be950 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 @@ -46,7 +46,7 @@ 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 diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt index 2d601544..b1074480 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt @@ -139,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 36a07bee..89976248 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 @@ -124,7 +124,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 00000000..7c7e2531 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ReactionInfoSimpleItem.kt @@ -0,0 +1,39 @@ +package im.vector.riotredesign.features.home.room.detail.timeline.action + +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 + + +@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 + + override fun bind(holder: Holder) { + holder.titleView.text = reactionKey + holder.displayNameView.text = authorDisplayName + timeStamp?.let { + holder.timeStampView.text = it + holder.timeStampView.isVisible = true + } ?: run { + holder.timeStampView.isVisible = false + } + } + + class Holder : VectorEpoxyHolder() { + val titleView 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 00000000..57645638 --- /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 00000000..a53dc3bd --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt @@ -0,0 +1,71 @@ +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.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +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.* + + +class ViewReactionBottomSheet : BaseMvRxBottomSheetDialog() { + + private val viewModel: ViewReactionViewModel by fragmentViewModel(ViewReactionViewModel::class) + + private val eventArgs: TimelineEventFragmentArgs by args() + + @BindView(R.id.bottom_sheet_display_reactions_list) + lateinit var epoxyRecyclerView: EpoxyRecyclerView + + private val epoxyController by lazy { ViewReactionsEpoxyController() } + + 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 00000000..a5e7fdd8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionViewModel.kt @@ -0,0 +1,106 @@ +package im.vector.riotredesign.features.home.room.detail.timeline.action + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import com.airbnb.mvrx.* +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary +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 kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +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, + lifecycleOwner: LifecycleOwner?, + liveSummary: LiveData>?, + initialState: DisplayReactionsViewState) : VectorViewModel(initialState) { + + init { + loadReaction() + if (lifecycleOwner != null) { + liveSummary?.observe(lifecycleOwner, Observer { + it?.firstOrNull()?.let { + loadReaction() + } + }) + } + + } + + private fun loadReaction() = withState { state -> + + GlobalScope.launch { + try { + val room = session.getRoom(state.roomId) + val event = room?.getTimeLineEvent(state.eventId) + if (event == null) { + setState { copy(mapReactionKeyToMemberList = Fail(Throwable())) } + return@launch + } + 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 + return ViewReactionViewModel(session, viewModelContext.activity.get(), viewModelContext.activity, session.getRoom(state.roomId)?.getEventSummaryLive(eventId), state) + } + + + } +} \ 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 00000000..a3d146a2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionsEpoxyController.kt @@ -0,0 +1,19 @@ +package im.vector.riotredesign.features.home.room.detail.timeline.action + +import com.airbnb.epoxy.TypedEpoxyController + + +class ViewReactionsEpoxyController : TypedEpoxyController() { + + override fun buildModels(state: DisplayReactionsViewState) { + val map = state.mapReactionKeyToMemberList() ?: return + map.forEach { + reactionInfoSimpleItem { + id(it.eventId) + 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/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt index b74f7bca..a3f43baf 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 @@ -65,6 +65,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) { @@ -112,7 +116,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 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 b0bf08e3..c0716cf2 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 @@ -37,13 +37,14 @@ 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() @@ -74,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) } @@ -101,7 +102,7 @@ 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 @@ -136,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() } @@ -242,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 } /** @@ -335,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/res/layout/bottom_sheet_display_reactions.xml b/vector/src/main/res/layout/bottom_sheet_display_reactions.xml new file mode 100644 index 00000000..0f5b6365 --- /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/item_simple_reaction_info.xml b/vector/src/main/res/layout/item_simple_reaction_info.xml new file mode 100644 index 00000000..0b84aedc --- /dev/null +++ b/vector/src/main/res/layout/item_simple_reaction_info.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/reaction_button.xml b/vector/src/main/res/layout/reaction_button.xml index aca0d2bf..6c169297 100644 --- a/vector/src/main/res/layout/reaction_button.xml +++ b/vector/src/main/res/layout/reaction_button.xml @@ -2,16 +2,19 @@ - + + + + + + tools:text="13450" /> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 3554d619..8109c89d 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -20,6 +20,7 @@ Agree Like Add Reaction + Reactions Event deleted by user Event moderated by room admin From 297f202005100d79fd8478e9c8431b6075728350 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 5 Jun 2019 23:42:08 +0200 Subject: [PATCH 05/18] Fix / Local echo taking too much time --- .../internal/session/room/EventRelationsAggregationTask.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 a66b65ab..e5fb5493 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 @@ -28,6 +28,7 @@ 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 @@ -48,7 +49,7 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc private val SHOULD_HANDLE_SERVER_AGREGGATION = false override fun execute(params: EventRelationsAggregationTask.Params): Try { - return monarchy.tryTransactionAsync { realm -> + return monarchy.tryTransactionSync { realm -> update(realm, params.events, params.userId) } } From e22b555b58f5a79ed5669a9d64a3bf4b346078e4 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 5 Jun 2019 23:45:46 +0200 Subject: [PATCH 06/18] Refactoring (duplication in Message Item Factory) + cleaning --- .../timeline/TimelineEventController.kt | 9 ++- .../timeline/action/ViewReactionViewModel.kt | 81 +++++++++---------- .../timeline/factory/MessageItemFactory.kt | 68 ++-------------- .../detail/timeline/item/AbsMessageItem.kt | 19 +++-- .../res/layout/item_simple_reaction_info.xml | 4 +- 5 files changed, 69 insertions(+), 112 deletions(-) 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 175702cf..2861ae6a 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,7 +46,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler() ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { - interface Callback : ReactionPillCallback { + interface Callback : ReactionPillCallback, AvatarCallback { fun onEventVisible(event: TimelineEvent) fun onUrlClicked(url: String) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) @@ -55,8 +55,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, 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?) } @@ -65,6 +63,11 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) } + interface AvatarCallback { + fun onAvatarClicked(informationData: MessageInformationData) + fun onMemberNameClicked(informationData: MessageInformationData) + } + private val collapsedEventIds = linkedSetOf() private val mergeItemCollapseStates = HashMap() private val modelCache = arrayListOf() 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 index a5e7fdd8..88548865 100644 --- 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 @@ -1,16 +1,12 @@ package im.vector.riotredesign.features.home.room.detail.timeline.action -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData +import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import com.airbnb.mvrx.* import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary 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 kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import org.koin.android.ext.android.get @@ -33,53 +29,41 @@ data class ReactionInfo( */ class ViewReactionViewModel(private val session: Session, private val timelineDateFormatter: TimelineDateFormatter, - lifecycleOwner: LifecycleOwner?, - liveSummary: LiveData>?, initialState: DisplayReactionsViewState) : VectorViewModel(initialState) { init { loadReaction() - if (lifecycleOwner != null) { - liveSummary?.observe(lifecycleOwner, Observer { - it?.firstOrNull()?.let { - loadReaction() - } - }) - } - } - private fun loadReaction() = withState { state -> + fun loadReaction() = withState { state -> - GlobalScope.launch { - try { - val room = session.getRoom(state.roomId) - val event = room?.getTimeLineEvent(state.eventId) - if (event == null) { - setState { copy(mapReactionKeyToMemberList = Fail(Throwable())) } - return@launch - } - var results = ArrayList() - event.annotations?.reactionsSummary?.forEach { sum -> + 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) - ) + 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) + ) + } } } @@ -98,7 +82,18 @@ class ViewReactionViewModel(private val session: Session, override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionViewModel? { val session = viewModelContext.activity.get() val eventId = (viewModelContext.args as TimelineEventFragmentArgs).eventId - return ViewReactionViewModel(session, viewModelContext.activity.get(), viewModelContext.activity, session.getRoom(state.roomId)?.getEventSummaryLive(eventId), state) + 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 } 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 7571fead..3390f4c2 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 @@ -143,18 +143,11 @@ 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) .emojiTypeFace(emojiCompatFontProvider.typeface) - .avatarClickListener( - DebouncedClickListener(View.OnClickListener { - callback?.onAvatarClicked(informationData) - })) - .memberClickListener( - DebouncedClickListener(View.OnClickListener { - callback?.onMemberNameClicked(informationData) - })) .cellClickListener( DebouncedClickListener(View.OnClickListener { view: View -> callback?.onEventCellClicked(informationData, messageContent, view) @@ -174,18 +167,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) @@ -223,17 +209,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageImageVideoItem_() .playable(messageContent.info?.mimeType == "image/gif") .informationData(informationData) + .avatarCallback(callback) .mediaData(data) .reactionPillCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) - .avatarClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onAvatarClicked(informationData) - })) - .memberClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onMemberNameClicked(informationData) - })) .clickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onImageMessageClicked(messageContent, data, view) @@ -271,17 +250,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageImageVideoItem_() .playable(true) .informationData(informationData) + .avatarCallback(callback) .mediaData(thumbnailData) .reactionPillCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) - .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) @@ -316,16 +288,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider, } } .informationData(informationData) + .avatarCallback(callback) .reactionPillCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) - .avatarClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onAvatarClicked(informationData) - })) - .memberClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onMemberNameClicked(informationData) - })) //click on the text .clickListener( DebouncedClickListener(View.OnClickListener { view -> @@ -390,12 +355,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageTextItem_() .message(message) .informationData(informationData) + .avatarCallback(callback) .reactionPillCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) - .avatarClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onAvatarClicked(informationData) - })) .memberClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onMemberNameClicked(informationData) @@ -430,16 +392,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider, } } .informationData(informationData) + .avatarCallback(callback) .reactionPillCallback(callback) .emojiTypeFace(emojiCompatFontProvider.typeface) - .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) @@ -454,14 +409,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/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt index a3f43baf..7749e152 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 @@ -29,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 @@ -45,9 +46,6 @@ abstract class AbsMessageItem : BaseEventItem() { @EpoxyAttribute var cellClickListener: View.OnClickListener? = null - @EpoxyAttribute - var avatarClickListener: View.OnClickListener? = null - @EpoxyAttribute var memberClickListener: View.OnClickListener? = null @@ -57,6 +55,17 @@ abstract class AbsMessageItem : BaseEventItem() { @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) @@ -81,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 diff --git a/vector/src/main/res/layout/item_simple_reaction_info.xml b/vector/src/main/res/layout/item_simple_reaction_info.xml index 0b84aedc..0458b171 100644 --- a/vector/src/main/res/layout/item_simple_reaction_info.xml +++ b/vector/src/main/res/layout/item_simple_reaction_info.xml @@ -11,8 +11,10 @@ Date: Thu, 6 Jun 2019 11:54:08 +0200 Subject: [PATCH 07/18] Show text with only few emojis in bigger --- .../riotredesign/core/utils/DimensionUtils.kt | 8 +++ .../vector/riotredesign/core/utils/Emoji.kt | 69 +++++++++++++++++++ .../detail/timeline/item/MessageTextItem.kt | 18 +++-- 3 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotredesign/core/utils/Emoji.kt 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 ab3654a3..44659da6 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 00000000..3595ad87 --- /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/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 790243c4..b6f77901 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 @@ -16,10 +16,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item -import android.text.Spannable import android.view.View -import android.widget.ImageView -import android.widget.TextView import androidx.appcompat.widget.AppCompatTextView import androidx.core.text.PrecomputedTextCompat import androidx.core.text.toSpannable @@ -28,6 +25,8 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.riotredesign.R +import im.vector.riotredesign.core.utils.DimensionUtils +import im.vector.riotredesign.core.utils.containsOnlyEmojis import im.vector.riotredesign.features.html.PillImageSpan import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -47,12 +46,21 @@ abstract class MessageTextItem : AbsMessageItem() { override fun bind(holder: Holder) { super.bind(holder) MatrixLinkify.addLinkMovementMethod(holder.messageView) - val textFuture = PrecomputedTextCompat.getTextFuture(message ?: "", + + val msg = message ?: "" + if (msg.length <= 4 && containsOnlyEmojis(msg.toString())) { + holder.messageView.textSize = 44F + } else { + holder.messageView.textSize = 14F + } + + val textFuture = PrecomputedTextCompat.getTextFuture(msg, TextViewCompat.getTextMetricsParams(holder.messageView), null) + holder.messageView.setTextFuture(textFuture) holder.messageView.renderSendState() - holder.messageView.setOnClickListener (clickListener) + holder.messageView.setOnClickListener(clickListener) holder.messageView.setOnLongClickListener(longClickListener) findPillsAndProcess { it.bind(holder.messageView) } } From 053dc1d8dda281c1adccc0e32070e91f870b3f58 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 6 Jun 2019 11:55:26 +0200 Subject: [PATCH 08/18] Show 'view reaction' option in context menu --- .../home/room/detail/RoomDetailFragment.kt | 5 +++ .../home/room/detail/RoomDetailViewModel.kt | 6 +-- .../timeline/action/MessageMenuViewModel.kt | 27 ++++++++----- .../main/res/drawable/ic_view_reactions.xml | 38 +++++++++++++++++++ .../main/res/drawable/rounded_rect_shape.xml | 2 +- vector/src/main/res/values/strings_riotX.xml | 1 + 6 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_view_reactions.xml 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 5ab0e46c..3d3667b3 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 @@ -621,6 +621,11 @@ 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) 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 7a47cff0..351129a2 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 @@ -195,7 +195,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 +331,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( @@ -352,7 +352,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, 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 -> + room.getTimeLineEvent(it)?.let { event -> displayedEventsObservable.accept(RoomDetailActions.EventDisplayed(event)) } } 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 776be950..6a77c970 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 @@ -50,7 +50,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel().apply { if (event.sendState == SendState.SENDING) { @@ -94,10 +92,13 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel true - else -> false + else -> false } } @@ -159,7 +160,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel { true } - else -> false + else -> false } } @@ -170,6 +171,13 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel { true } - else -> false + else -> false } } @@ -203,7 +211,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel { true } - else -> false + else -> false } } @@ -220,6 +228,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel + + + + + diff --git a/vector/src/main/res/drawable/rounded_rect_shape.xml b/vector/src/main/res/drawable/rounded_rect_shape.xml index cf083254..e9c0a27f 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/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 8109c89d..8d39ffe7 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -20,6 +20,7 @@ Agree Like Add Reaction + View Reactions Reactions Event deleted by user From 04576ba7fdef0fb7425c3512cdb5f5312ce9d203 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 6 Jun 2019 12:26:38 +0200 Subject: [PATCH 09/18] Permalink message action + Fix crash on injection of navigator --- .../vector/riotredesign/core/di/AppModule.kt | 5 ++-- .../features/home/HomeDrawerFragment.kt | 2 +- .../features/home/HomeNavigator.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 18 ++++++++---- .../timeline/action/MessageMenuViewModel.kt | 6 ++-- .../home/room/list/RoomListFragment.kt | 4 +-- .../features/navigation/DefaultNavigator.kt | 28 +++++++++---------- .../features/navigation/Navigator.kt | 9 +++--- .../roomdirectory/PublicRoomsFragment.kt | 4 +-- .../RoomPreviewNoPreviewFragment.kt | 2 +- 10 files changed, 44 insertions(+), 36 deletions(-) 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 542c0953..28ae4e1a 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,7 +18,6 @@ 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 @@ -87,8 +86,8 @@ class AppModule(private val context: Context) { Matrix.getInstance().currentSession!! } - factory { (fragment: Fragment) -> - DefaultNavigator(fragment) as Navigator + factory { + DefaultNavigator() as Navigator } single { 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 ff08720d..97baa539 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,7 +54,7 @@ class HomeDrawerFragment : VectorBaseFragment() { } } homeDrawerHeaderSettingsView.setOnClickListener { - navigator.openSettings() + navigator.openSettings(requireActivity()) } // Debug menu 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 8d29c0ab..e46fd7f5 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 3d3667b3..1da6ed9d 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 @@ -622,16 +623,16 @@ class RoomDetailFragment : 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) + 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 @@ -698,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/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index 6a77c970..ca422f72 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 @@ -64,7 +64,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel().apply { if (event.sendState == SendState.SENDING) { @@ -123,7 +123,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel { - 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 07499555..3f389a46 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 From 3f1bf00fdd3c5a746088c1983d8cd1e68aa4f60f Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 6 Jun 2019 12:59:26 +0200 Subject: [PATCH 10/18] Fix / use emoji Compat font for view reaction screen --- .../timeline/action/ReactionInfoSimpleItem.kt | 13 ++++++++++--- .../timeline/action/ViewReactionBottomSheet.kt | 11 +++++++---- .../timeline/action/ViewReactionsEpoxyController.kt | 8 ++++++-- 3 files changed, 23 insertions(+), 9 deletions(-) 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 index 7c7e2531..647af4b0 100644 --- 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 @@ -1,5 +1,6 @@ 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 @@ -8,7 +9,9 @@ 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() { @@ -19,8 +22,12 @@ abstract class ReactionInfoSimpleItem : EpoxyModelWithHolder(R.id.itemSimpleReactionInfoKey) + 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/ViewReactionBottomSheet.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ViewReactionBottomSheet.kt index a53dc3bd..308c68bf 100644 --- 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 @@ -11,24 +11,27 @@ import butterknife.BindView import butterknife.ButterKnife import com.airbnb.epoxy.EpoxyRecyclerView import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.args 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 eventArgs: TimelineEventFragmentArgs by args() + private val emojiCompatFontProvider by inject() @BindView(R.id.bottom_sheet_display_reactions_list) lateinit var epoxyRecyclerView: EpoxyRecyclerView - private val epoxyController by lazy { ViewReactionsEpoxyController() } + 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) 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 index a3d146a2..461fec2b 100644 --- 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 @@ -1,15 +1,19 @@ package im.vector.riotredesign.features.home.room.detail.timeline.action +import android.graphics.Typeface import com.airbnb.epoxy.TypedEpoxyController - -class ViewReactionsEpoxyController : 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) From 740900394916a91ccd112a0366dec6953b102c0c Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 7 Jun 2019 10:01:42 +0200 Subject: [PATCH 11/18] Fix / Bug aggregation on initial sync fix / All messages were not processed due to a test exiting the for loop + started adding context menu for non room messages --- matrix-sdk-android/build.gradle | 4 +- .../internal/database/mapper/EventMapper.kt | 13 +- .../room/EventRelationsAggregationTask.kt | 198 ++++++++++-------- .../room/EventRelationsAggregationUpdater.kt | 6 +- .../android/internal/task/TaskExecutor.kt | 7 +- vector/build.gradle | 6 +- .../home/room/detail/RoomDetailFragment.kt | 4 +- .../home/room/detail/RoomDetailViewModel.kt | 3 +- .../timeline/TimelineEventController.kt | 9 +- .../timeline/action/MessageMenuViewModel.kt | 22 +- .../timeline/factory/NoticeItemFactory.kt | 19 +- .../timeline/factory/TimelineItemFactory.kt | 35 +++- .../helper/TimelineDisplayableEvents.kt | 9 + .../room/detail/timeline/item/NoticeItem.kt | 29 ++- .../item_timeline_event_base_noinfo.xml | 3 +- 15 files changed, 233 insertions(+), 134 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 1a0c0f91..3fda5a63 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -48,7 +48,7 @@ android { 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.BASIC" + buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE" } release { @@ -91,7 +91,7 @@ dependencies { def moshi_version = '1.8.0' def lifecycle_version = '2.0.0' def coroutines_version = "1.0.1" - def markwon_version = '3.0.0-SNAPSHOT' + def markwon_version = '3.0.0' implementation fileTree(dir: 'libs', include: ['*.aar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 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 3c21437e..34b29187 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/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index e5fb5493..3b024942 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,6 @@ 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 @@ -49,60 +48,75 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc private val SHOULD_HANDLE_SERVER_AGREGGATION = false override fun execute(params: EventRelationsAggregationTask.Params): Try { + val events = params.events + val userId = params.userId return monarchy.tryTransactionSync { realm -> - update(realm, params.events, params.userId) + 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) { @@ -112,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 } @@ -120,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() @@ -181,62 +195,70 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc } 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 0b29064c..a04ed216 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/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt index a6855817..caf75bd7 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 4cec152f..263df78e 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -84,13 +84,15 @@ android { debug { resValue "bool", "debug_mode", "true" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" - + buildConfigField "boolean", "SHOW_HIDDEN_TIMELINE_EVENTS", "false" + signingConfig signingConfigs.debug } 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' @@ -132,7 +134,7 @@ dependencies { def epoxy_version = "3.3.0" def arrow_version = "0.8.2" def coroutines_version = "1.0.1" - def markwon_version = '3.0.0-SNAPSHOT' + def markwon_version = '3.0.0' def big_image_viewer_version = '1.5.6' def glide_version = '4.9.0' def moshi_version = '1.8.0' 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 1da6ed9d..55284325 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 @@ -563,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 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 351129a2..346d21c3 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,8 @@ 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 timeline = room.createTimeline(eventId, + if (TimelineDisplayableEvents.DEBUG_HIDDEN_EVENT) TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES else TimelineDisplayableEvents.DISPLAYABLE_TYPES) companion object : MvRxViewModelFactory { 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 2861ae6a..2a436a19 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,15 +46,13 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler() ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { - interface Callback : ReactionPillCallback, AvatarCallback { + 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 onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) } @@ -63,6 +61,11 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, 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) 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 ca422f72..6d99d41a 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...) @@ -50,9 +50,9 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel noticeItemFactory.create(event) + EventType.CALL_ANSWER -> noticeItemFactory.create(event, callback) // Unhandled event types (yet) EventType.ENCRYPTED, @@ -51,9 +57,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 657d9e03..711b870e 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/NoticeItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt index dcb0bdf4..914cb232 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,37 @@ 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 +61,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/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index 07b43f7b..8163b3ac 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 @@ Date: Fri, 7 Jun 2019 10:45:24 +0200 Subject: [PATCH 12/18] Menu action for non room messages --- .../action/MessageActionsBottomSheet.kt | 31 ++++++++++++------- .../action/MessageActionsViewModel.kt | 27 +++++++++------- .../timeline/action/MessageMenuViewModel.kt | 11 +++++-- .../timeline/action/QuickReactionViewModel.kt | 21 ++++++------- .../room/detail/timeline/item/NoticeItem.kt | 1 - .../layout/bottom_sheet_message_actions.xml | 15 ++++++--- 6 files changed, 64 insertions(+), 42 deletions(-) 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 fa94043e..7ea6a9b1 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 @@ -23,6 +23,7 @@ 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 @@ -35,6 +36,7 @@ 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.synthetic.main.bottom_sheet_message_actions.* /** * Bottom sheet fragment that shows a message preview with list of contextual actions @@ -115,20 +117,27 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() { } override fun invalidate() = withState(viewModel) { - senderNameTextView.text = it.senderName - messageBodyTextView.text = it.messageBody - messageTimestampText.text = it.ts + if (it.showPreview) { + bottom_sheet_message_preview.isVisible = true + 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) + 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) + } else { + senderAvatarImageView.setImageDrawable(AvatarRenderer.getPlaceholderDrawable(requireContext(), it.userId, it.senderName)) + } } 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 } 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 a307afe3..2a925894 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 @@ -19,6 +19,7 @@ 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 @@ -34,10 +35,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 @@ -59,21 +62,21 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel() ?: event.root.content.toModel() val originTs = event.root.originServerTs - var body: CharSequence = messageContent?.body ?: "" + 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 ?: messageContent.body) - // val renderer = HtmlRenderer.builder().build() body = Markwon.builder(viewModelContext.activity) .usePlugin(HtmlPlugin.create()).build().render(document) -// body = renderer.render(document) } 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 = event.root.type == EventType.MESSAGE, + canReact = event.root.type == EventType.MESSAGE, + senderAvatarPath = currentSession.contentUrlResolver().resolveFullSize(parcel.informationData.avatarUrl) ) } else { //can this happen? 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 6d99d41a..d2511eff 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 @@ -73,7 +73,9 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel>? = 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 } } 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 914cb232..d190875f 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 @@ -37,7 +37,6 @@ abstract class NoticeItem : BaseEventItem() { @EpoxyAttribute var baseCallback: TimelineEventController.BaseCallback? = null - private var longClickListener = View.OnLongClickListener { baseCallback?.onEventLongClicked(informationData, null, it) baseCallback != null 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 0de471bd..d1cb8c9f 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" /> From 651d0472cd408d963cca18bd189108d16ced9c32 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 7 Jun 2019 11:18:14 +0200 Subject: [PATCH 13/18] Show preview for notice events in context menu + fix merge issues --- vector/build.gradle | 2 +- .../features/home/HomeDrawerFragment.kt | 2 +- .../action/MessageActionsViewModel.kt | 42 ++++++++++++++----- .../features/navigation/DefaultNavigator.kt | 4 +- .../features/navigation/Navigator.kt | 2 +- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index 263df78e..f411f064 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -85,7 +85,7 @@ android { resValue "bool", "debug_mode", "true" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "SHOW_HIDDEN_TIMELINE_EVENTS", "false" - + signingConfig signingConfigs.debug } 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 97baa539..66195881 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 @@ -59,7 +59,7 @@ class HomeDrawerFragment : VectorBaseFragment() { // 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/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 2a925894..1c293140 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,6 +15,7 @@ */ 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 @@ -25,8 +26,10 @@ 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.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 @@ -53,28 +56,45 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode override fun initialState(viewModelContext: ViewModelContext): MessageActionState? { val currentSession = viewModelContext.activity.get() + 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 ?: messageContent.body) - body = Markwon.builder(viewModelContext.activity) - .usePlugin(HtmlPlugin.create()).build().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( userId = event.root.sender ?: "", - senderName = parcel.informationData.memberName.toString(), + senderName = parcel.informationData.memberName?.toString() ?: "", messageBody = body, ts = dateFormat.format(Date(originTs ?: 0)), - showPreview = event.root.type == EventType.MESSAGE, + showPreview = body != null, canReact = event.root.type == EventType.MESSAGE, senderAvatarPath = currentSession.contentUrlResolver().resolveFullSize(parcel.informationData.avatarUrl) ) 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 00207950..44c1cfe3 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 @@ -52,7 +52,7 @@ class DefaultNavigator : Navigator { 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 02295964..8873227b 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 @@ -29,6 +29,6 @@ interface Navigator { fun openSettings(context: Context) - fun openDebug() + fun openDebug(context: Context) } \ No newline at end of file From 438404b5ba7f0ac3a418d10e4536e8560a0be6d0 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 7 Jun 2019 13:55:32 +0200 Subject: [PATCH 14/18] code review cleaning --- .../home/room/detail/RoomDetailViewModel.kt | 8 ++++++-- .../timeline/action/MessageActionsBottomSheet.kt | 13 +------------ .../timeline/action/MessageActionsViewModel.kt | 2 +- .../features/reactions/widget/ReactionButton.kt | 4 ++-- 4 files changed, 10 insertions(+), 17 deletions(-) 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 346d21c3..476ff99b 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,8 +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, - if (TimelineDisplayableEvents.DEBUG_HIDDEN_EVENT) TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES else 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 { 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 7ea6a9b1..9c9bbc2d 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 @@ -33,7 +33,6 @@ 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.synthetic.main.bottom_sheet_message_actions.* @@ -122,17 +121,7 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() { 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) - } else { - senderAvatarImageView.setImageDrawable(AvatarRenderer.getPlaceholderDrawable(requireContext(), it.userId, it.senderName)) - } + AvatarRenderer.render(it.senderAvatarPath, it.userId, it.senderName, senderAvatarImageView) } else { bottom_sheet_message_preview.isVisible = false } 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 1c293140..d0ddb854 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 @@ -96,7 +96,7 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode ts = dateFormat.format(Date(originTs ?: 0)), showPreview = body != null, canReact = event.root.type == EventType.MESSAGE, - senderAvatarPath = currentSession.contentUrlResolver().resolveFullSize(parcel.informationData.avatarUrl) + senderAvatarPath = parcel.informationData.avatarUrl ) } else { //can this happen? 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 c0716cf2..8250b668 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 @@ -25,7 +25,6 @@ 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 @@ -44,7 +43,8 @@ import im.vector.riotredesign.core.utils.TextUtils * 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, View.OnLongClickListener { + defStyleAttr: Int = 0) + : FrameLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener { companion object { private val DECCELERATE_INTERPOLATOR = DecelerateInterpolator() From 5f34e58bd3c046db66138692a2c18067f99c6b90 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 7 Jun 2019 14:29:42 +0200 Subject: [PATCH 15/18] Fix / style on emoji picker appbar layout --- .../res/layout/activity_emoji_reaction_picker.xml | 12 +++++------- vector/src/main/res/values/styles_riot.xml | 4 ++++ 2 files changed, 9 insertions(+), 7 deletions(-) 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 38694933..6611f266 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"> + android:layout_height="40dp" /> diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index fb536a60..1cd18d5e 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" + +