diff --git a/build.gradle b/build.gradle index d07b849e..c54c0f59 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,8 @@ buildscript { jcenter() maven { url "https://plugins.gradle.org/m2/" - } } + } + } dependencies { classpath 'com.android.tools.build:gradle:3.3.2' classpath 'com.google.gms:google-services:4.2.0' diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomEventService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomEventService.kt new file mode 100644 index 00000000..04df1cf9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomEventService.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.room + +import im.vector.matrix.android.api.session.events.model.Event + +interface RoomEventService { + + fun getEvent(eventId: String?) : Event +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt index 16cb7493..1d6880b0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.room.model.message + interface MessageContent { val type: String val body: String diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt index b5b2264e..e79a2db3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt @@ -18,9 +18,13 @@ package im.vector.matrix.android.api.session.room.send enum class SendState { UNKNOWN, + // the event has not been sent UNSENT, + // the event is encrypting ENCRYPTING, + // the event is currently sending SENDING, + // the event has been sent SENT, SYNCED; diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 58044278..7c73d761 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.timeline import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.send.SendState @@ -59,4 +60,8 @@ data class TimelineEvent( inline fun getMetadata(key: String): T? { return metadata[key] as T? } + + fun isEncrypted() : Boolean { + return EventType.ENCRYPTED == root.type + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt index 0980cfc5..295ddd07 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -30,4 +30,6 @@ interface TimelineService { */ fun createTimeline(eventId: String?, allowedTypes: List? = null): Timeline + + fun getTimeLineEvent(eventId: String): TimelineEvent? } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt index d07175d4..de2d3de0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt @@ -28,17 +28,18 @@ import im.vector.matrix.android.internal.database.query.findIncludingEvent import im.vector.matrix.android.internal.database.query.next import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where +import io.realm.Realm import io.realm.RealmList import io.realm.RealmQuery internal class SenderRoomMemberExtractor(private val roomId: String) { - fun extractFrom(event: EventEntity): RoomMember? { + fun extractFrom(event: EventEntity, realm: Realm = event.realm): RoomMember? { val sender = event.sender ?: return null // If the event is unlinked we want to fetch unlinked state events val unlinked = event.isUnlinked - val roomEntity = RoomEntity.where(event.realm, roomId = roomId).findFirst() ?: return null - val chunkEntity = ChunkEntity.findIncludingEvent(event.realm, event.eventId) + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return null + val chunkEntity = ChunkEntity.findIncludingEvent(realm, event.eventId) val content = when { chunkEntity == null -> null event.stateIndex <= 0 -> baseQuery(chunkEntity.events, sender, unlinked).next(from = event.stateIndex)?.prevContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 9700d712..4ca41ba7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -18,8 +18,12 @@ package im.vector.matrix.android.internal.session.room.timeline import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineService +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.util.fetchMappedCopied internal class DefaultTimelineService(private val roomId: String, private val monarchy: Monarchy, @@ -33,4 +37,12 @@ internal class DefaultTimelineService(private val roomId: String, return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, allowedTypes) } + override fun getTimeLineEvent(eventId: String): TimelineEvent? { + return monarchy.fetchMappedCopied({ + EventEntity.where(it, eventId = eventId).findFirst() + }, { entity, realm -> + timelineEventFactory.create(entity, realm) + }) + } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt index 28cd2a9d..7fe38391 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt @@ -20,16 +20,17 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor +import io.realm.Realm internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomMemberExtractor) { private val cached = mutableMapOf() - fun create(eventEntity: EventEntity): TimelineEvent { + fun create(eventEntity: EventEntity, realm: Realm = eventEntity.realm): TimelineEvent { val sender = eventEntity.sender val cacheKey = sender + eventEntity.stateIndex val senderData = cached.getOrPut(cacheKey) { - val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity) + val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity,realm) SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl) } return TimelineEvent( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt index 2a0fc5d7..572b8434 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt @@ -42,6 +42,17 @@ fun Monarchy.fetchCopied(query: (Realm) -> T?): T? { return fetch(query, true) } +fun Monarchy.fetchMappedCopied(query: (Realm) -> T?, map: (T, realm: Realm) -> U): U? { + val ref = AtomicReference() + doWithRealm { realm -> + val result = query.invoke(realm)?.let { + map(realm.copyFromRealm(it), realm) + } + ref.set(result) + } + return ref.get() +} + private fun Monarchy.fetch(query: (Realm) -> T?, copyFromRealm: Boolean): T? { val ref = AtomicReference() doWithRealm { realm -> diff --git a/reactions/build.gradle b/reactions/build.gradle index 74e8f2ae..4b52ec4b 100644 --- a/reactions/build.gradle +++ b/reactions/build.gradle @@ -12,6 +12,7 @@ android { targetSdkVersion 28 versionCode 1 versionName "1.0" + multiDexEnabled true testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -36,6 +37,9 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + // Log + implementation 'com.jakewharton.timber:timber:4.7.1' + implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'androidx.appcompat:appcompat:1.0.0-beta01' diff --git a/reactions/src/main/AndroidManifest.xml b/reactions/src/main/AndroidManifest.xml index d8755d4b..3ada8841 100644 --- a/reactions/src/main/AndroidManifest.xml +++ b/reactions/src/main/AndroidManifest.xml @@ -3,6 +3,10 @@ package="im.vector.reactions"> + + = MutableLiveData() + + val currentSection: MutableLiveData = MutableLiveData() fun initWithContect(context: Context) { - adapter = EmojiRecyclerAdapter(EmojiDataSource(context)) + val emojiDataSource = EmojiDataSource(context) + emojiSourceLiveData.value = emojiDataSource + adapter = EmojiRecyclerAdapter(emojiDataSource) + adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener { + override fun firstVisibleSectionChange(section: Int) { + currentSection.value = section + } + + } } + fun scrollToSection(sectionIndex: Int) { + adapter?.scrollToSection(sectionIndex) + } } diff --git a/reactions/src/main/java/im/vector/reactions/EmojiDrawView.kt b/reactions/src/main/java/im/vector/reactions/EmojiDrawView.kt index 4380986a..cc22bd0d 100644 --- a/reactions/src/main/java/im/vector/reactions/EmojiDrawView.kt +++ b/reactions/src/main/java/im/vector/reactions/EmojiDrawView.kt @@ -3,17 +3,14 @@ package im.vector.reactions import android.content.Context import android.graphics.Canvas import android.graphics.Color +import android.graphics.Typeface +import android.text.Layout import android.text.StaticLayout import android.text.TextPaint import android.util.AttributeSet import android.view.View -import android.text.Layout -import android.util.Log -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import java.lang.Exception import kotlin.math.abs -import kotlin.math.max /** @@ -25,29 +22,26 @@ class EmojiDrawView @JvmOverloads constructor( ) : View(context, attrs, defStyleAttr) { var mLayout: StaticLayout? = null + set(value) { + field = value + invalidate() + } // var _mySpacing = 0f var emoji: String? = null - set(value) { - field = value - if (value != null) { - EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.TextStaticLayout") -// GlobalScope.launch { -// val sl = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true) -// GlobalScope.launch(Dispatchers.Main) { -// if (emoji == value) { -// mLayout = sl -// //invalidate() -// } -// } -// } - mLayout = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true) - EmojiRecyclerAdapter.endTraceSession() - } else { - mLayout = null - } - } +// set(value) { +// if (value != null) { +// EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.TextStaticLayout") +// mLayout = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true) +// if (value != field) invalidate() +// EmojiRecyclerAdapter.endTraceSession() +// } else { +// mLayout = null +//// if (value != field) invalidate() +// } +// field = value +// } override fun onDraw(canvas: Canvas?) { EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.onDraw") @@ -55,7 +49,7 @@ class EmojiDrawView @JvmOverloads constructor( canvas?.save() val space = abs((width - emojiSize) / 2f) if (mLayout == null) { - canvas?.drawCircle(width / 2f ,width / 2f, emojiSize / 2f,tPaint) +// canvas?.drawCircle(width / 2f ,width / 2f, emojiSize / 2f,tPaint) } else { canvas?.translate(space, space) mLayout!!.draw(canvas) @@ -65,14 +59,18 @@ class EmojiDrawView @JvmOverloads constructor( } companion object { - private val tPaint = TextPaint() + val tPaint = TextPaint() - private var emojiSize = 40 + var emojiSize = 40 - fun configureTextPaint(context: Context) { + fun configureTextPaint(context: Context, typeface: Typeface?) { tPaint.isAntiAlias = true; tPaint.textSize = 24 * context.resources.displayMetrics.density tPaint.color = Color.LTGRAY + typeface?.let { + tPaint.typeface = it + } + emojiSize = tPaint.measureText("😅").toInt() } } diff --git a/reactions/src/main/java/im/vector/reactions/EmojiReactionPickerActivity.kt b/reactions/src/main/java/im/vector/reactions/EmojiReactionPickerActivity.kt index a4a4b2e0..40b7f530 100644 --- a/reactions/src/main/java/im/vector/reactions/EmojiReactionPickerActivity.kt +++ b/reactions/src/main/java/im/vector/reactions/EmojiReactionPickerActivity.kt @@ -15,34 +15,121 @@ */ package im.vector.reactions +import android.content.Context +import android.content.Intent +import android.graphics.Typeface import android.os.Bundle +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.app.AppCompatActivity +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 timber.log.Timber + +/** + * + * TODO: Loading indicator while getting emoji data source? + * TODO: migrate to maverick + */ class EmojiReactionPickerActivity : AppCompatActivity() { - lateinit var tabLayout: TabLayout + private lateinit var tabLayout: TabLayout + + lateinit var viewModel: EmojiChooserViewModel + + private var mHandler: Handler? = null + + private var tabLayoutSelectionListener = object : TabLayout.BaseOnTabSelectedListener { + override fun onTabReselected(p0: TabLayout.Tab) { + } + + override fun onTabUnselected(p0: TabLayout.Tab) { + } + + override fun onTabSelected(p0: TabLayout.Tab) { + viewModel.scrollToSection(p0.position) + } + + } + + private fun getFontThreadHandler(): Handler { + if (mHandler == null) { + val handlerThread = HandlerThread("fonts") + handlerThread.start() + mHandler = Handler(handlerThread.looper) + } + return mHandler!! + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + requestEmojivUnicode10CompatibleFont() + + setContentView(R.layout.activity_emoji_reaction_picker) setSupportActionBar(findViewById(R.id.toolbar)) tabLayout = findViewById(R.id.tabs) - tabLayout.addTab(tabLayout.newTab().setText("Tab 1")); - tabLayout.addTab(tabLayout.newTab().setText("Tab 2")); - tabLayout.addTab(tabLayout.newTab().setText("Tab 3")); + viewModel = ViewModelProviders.of(this).get(EmojiChooserViewModel::class.java) + viewModel.emojiSourceLiveData.observe(this, Observer { + it.rawData?.categories?.let { categories -> + for (category in categories) { + val s = category.emojis[0] + tabLayout.addTab(tabLayout.newTab().setText(it.rawData!!.emojis[s]!!.emojiString())) + } + tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener) + } + + }) + + viewModel.currentSection.observe(this, Observer { section -> + section?.let { + tabLayout.removeOnTabSelectedListener(tabLayoutSelectionListener) + tabLayout.getTabAt(it)?.select() + tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener) + } + }) supportActionBar?.title = getString(R.string.title_activity_emoji_reaction_picker) } + 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 + ) + + 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 onCreateOptionsMenu(menu: Menu): Boolean { val inflater: MenuInflater = menuInflater inflater.inflate(R.menu.menu_emoji_reaction_picker, menu) @@ -54,20 +141,40 @@ class EmojiReactionPickerActivity : AppCompatActivity() { override fun onMenuItemActionExpand(p0: MenuItem?): Boolean { it.isIconified = false it.requestFocusFromTouch() + //we want to force the tool bar as visible even if hidden with scroll flags + findViewById(R.id.toolbar)?.minimumHeight = getActionBarSize() return true } override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean { // when back, clear all search + findViewById(R.id.toolbar)?.minimumHeight = 0 it.setQuery("", true) return true } }) } - return true } + //TODO move to ThemeUtils when core module is created + private fun getActionBarSize(): Int { + return try { + val typedValue = TypedValue() + theme.resolveAttribute(R.attr.actionBarSize, typedValue, true) + TypedValue.complexToDimensionPixelSize(typedValue.data, resources.displayMetrics) + } catch (e: Exception) { + //Timber.e(e, "Unable to get color") + TypedValue.complexToDimensionPixelSize(56, resources.displayMetrics) + } + } + companion object { + fun intent(context: Context): Intent { + val intent = Intent(context, EmojiReactionPickerActivity::class.java) +// intent.putExtra(EXTRA_MATRIX_ID, matrixID) + return intent + } + } } diff --git a/reactions/src/main/java/im/vector/reactions/EmojiRecyclerAdapter.kt b/reactions/src/main/java/im/vector/reactions/EmojiRecyclerAdapter.kt index 63f7f83e..55dacf59 100644 --- a/reactions/src/main/java/im/vector/reactions/EmojiRecyclerAdapter.kt +++ b/reactions/src/main/java/im/vector/reactions/EmojiRecyclerAdapter.kt @@ -15,66 +15,58 @@ */ package im.vector.reactions -import android.annotation.SuppressLint import android.os.Build import android.os.Trace +import android.text.Layout +import android.text.StaticLayout import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import android.widget.TextView import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.transition.AutoTransition +import androidx.transition.TransitionManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlin.math.abs +/** + * + * TODO: Configure Span using available width and emoji size + * TODO: Search + * TODO: Performances + * TODO: Scroll to section - Find a way to snap section to the top + */ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) : RecyclerView.Adapter() { -// data class EmojiInfo(val stringValue: String) -// data class SectionInfo(val sectionName: String) + var interactionListener: InteractionListener? = null + var mRecyclerView: RecyclerView? = null - //val mockData: ArrayList>> = ArrayList() - // val dataSource : EmojiDataSource? = null - - init { -// val faces = ArrayList() -// for (i in 0..50) { -// faces.add(EmojiInfo("😅")) -// } -// val animalsNature = ArrayList() -// for (i in 0..160) { -// animalsNature.add(EmojiInfo("🐶")) -// } -// val foods = ArrayList() -// for (i in 0..150) { -// foods.add(EmojiInfo("🍎")) -// } -// -// mockData.add(SectionInfo("Smiley & People") to faces) -// mockData.add(SectionInfo("Animals & Nature") to animalsNature) -// mockData.add(SectionInfo("Food & Drinks") to foods) -// dataSource = EMp + var currentFirstVisibleSection = 0 + enum class ScrollState { + IDLE, + DRAGGING, + SETTLING, + UNKNWON } -// enum class ScrollState { -// IDLE, -// DRAGGING, -// SETTLING, -// UNKNWON -// } + private var scrollState = ScrollState.UNKNWON + private var isFastScroll = false + + val toUpdateWhenNotBusy = ArrayList>() - private val scrollListener = object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - super.onScrollStateChanged(recyclerView, newState) - } - } override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) - - EmojiDrawView.configureTextPaint(recyclerView.context) + this.mRecyclerView = recyclerView val gridLayoutManager = GridLayoutManager(recyclerView.context, 8) gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { @@ -98,6 +90,24 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) : recyclerView.addOnScrollListener(scrollListener) } + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + this.mRecyclerView = null + recyclerView.removeOnScrollListener(scrollListener) + staticLayoutCache.clear() + super.onDetachedFromRecyclerView(recyclerView) + } + + fun scrollToSection(section: Int) { + if (section < 0 || section >= dataSource?.rawData?.categories?.size ?: 0) { + //ignore + return + } + //mRecyclerView?.smoothScrollToPosition(getSectionOffset(section) - 1) + //TODO Snap section header to top + mRecyclerView?.scrollToPosition(getSectionOffset(section) - 1) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { beginTraceSession("MyAdapter.onCreateViewHolder") val inflater = LayoutInflater.from(parent.context) @@ -162,7 +172,6 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) : return sectionOffset } - @SuppressLint("NewApi") override fun onBindViewHolder(holder: ViewHolder, position: Int) { beginTraceSession("MyAdapter.onBindViewHolder") dataSource?.rawData?.categories?.let { categories -> @@ -174,13 +183,36 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) : val sectionOffset = getSectionOffset(sectionNumber) val emoji = sectionMojis[position - sectionOffset] val item = dataSource!!.rawData!!.emojis[emoji]!!.emojiString() - holder.bind(item) + (holder as EmojiViewHolder).data = item + if (scrollState != ScrollState.SETTLING || !isFastScroll) { +// Log.i("PERF","Bind with draw at position:$position") + holder.bind(item) + } else { +// Log.i("PERF","Bind without draw at position:$position") + toUpdateWhenNotBusy.add(item to holder) + holder.bind(null) + } } } endTraceSession() - } + override fun onViewRecycled(holder: ViewHolder) { + if (holder is EmojiViewHolder) { + holder.data = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + toUpdateWhenNotBusy.removeIf { it.second == holder } + } else { + val index = toUpdateWhenNotBusy.indexOfFirst { it.second == holder } + if (index != -1) { + toUpdateWhenNotBusy.removeAt(index) + } + } + } + super.onViewRecycled(holder) + } + + override fun getItemCount(): Int { dataSource?.rawData?.categories?.let { var count = /*number of sections*/ it.size @@ -193,17 +225,28 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) : abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - abstract fun bind(s: String) + abstract fun bind(s: String?) } class EmojiViewHolder(itemView: View) : ViewHolder(itemView) { var emojiView: EmojiDrawView = itemView.findViewById(R.id.grid_item_emoji_text) + val placeHolder: View = itemView.findViewById(R.id.grid_item_place_holder) + var data: String? = null - override fun bind(s: String) { + override fun bind(s: String?) { emojiView.emoji = s + if (s != null) { + emojiView.mLayout = getStaticLayoutForEmoji(s) + placeHolder.visibility = View.GONE +// emojiView.visibility = View.VISIBLE + } else { + emojiView.mLayout = null + placeHolder.visibility = View.VISIBLE +// emojiView.visibility = View.GONE + } } } @@ -211,11 +254,10 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) : var textView: TextView = itemView.findViewById(R.id.section_header_textview) - override fun bind(s: String) { + override fun bind(s: String?) { textView.text = s } - } companion object { @@ -230,40 +272,75 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) : Trace.beginSection(sectionName) } } + + val staticLayoutCache = HashMap() + + fun getStaticLayoutForEmoji(emoji: String): StaticLayout { + var cachedLayout = staticLayoutCache[emoji] + if (cachedLayout == null) { + cachedLayout = StaticLayout(emoji, EmojiDrawView.tPaint, EmojiDrawView.emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true) + staticLayoutCache[emoji] = cachedLayout!! + } + return cachedLayout!! + } + + } + + interface InteractionListener { + fun firstVisibleSectionChange(section: Int) + } + + //privates + + private val scrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + scrollState = when (newState) { + RecyclerView.SCROLL_STATE_IDLE -> ScrollState.IDLE + RecyclerView.SCROLL_STATE_SETTLING -> ScrollState.SETTLING + RecyclerView.SCROLL_STATE_DRAGGING -> ScrollState.DRAGGING + else -> ScrollState.UNKNWON + } + + //TODO better + if (scrollState == ScrollState.IDLE) { + // + val toUpdate = toUpdateWhenNotBusy.clone() as ArrayList> + toUpdateWhenNotBusy.clear() + toUpdate.chunked(8).forEach { + recyclerView.post { + val transition = AutoTransition().apply { + duration = 150 + } + for (pair in it) { + val holder = pair.second + if (pair.first == holder.data) { + TransitionManager.beginDelayedTransition(holder.itemView as FrameLayout, transition) + val data = holder.data + holder.bind(data) + } + } + toUpdateWhenNotBusy.clear() + } + } + + } + } + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + //Log.i("SCROLL SPEED","scroll speed $dy") + isFastScroll = abs(dy) > 50 + val visible = (recyclerView.layoutManager as GridLayoutManager).findFirstCompletelyVisibleItemPosition() + GlobalScope.launch { + val section = getSectionForAbsoluteIndex(visible) + if (section != currentFirstVisibleSection) { + currentFirstVisibleSection = section + GlobalScope.launch(Dispatchers.Main) { + interactionListener?.firstVisibleSectionChange(currentFirstVisibleSection) + } + } + } + } } -// data class SectionsIndex(val dataSource: EmojiDataSource) { -// var sectionsIndex: ArrayList = ArrayList() -// var sectionsInfo: ArrayList> = ArrayList() -// var itemCount = 0 -// -// init { -// var sectionOffset = 1 -// var lastItemInSection = 0 -// dataSource.rawData?.categories?.let { -// for (category in it) { -// sectionsIndex.add(sectionOffset - 1) -// lastItemInSection = sectionOffset + category.emojis.size - 1 -// sectionsInfo.add(sectionOffset to lastItemInSection) -// sectionOffset = lastItemInSection + 2 -// itemCount += (1 + category.emojis.size) -// } -// } -// } -// -// fun getCount(): Int = this.itemCount -// -// fun isSection(position: Int): Int? { -// return sectionsIndex.indexOf(position) -// } -// -// fun getSectionForAbsoluteIndex(position: Int): Int { -// for (i in sectionsIndex.size - 1 downTo 0) { -// val sectionOffset = sectionsIndex[i] -// if (position >= sectionOffset) { -// return i -// } -// } -// return 0 -// } -// } } \ No newline at end of file diff --git a/reactions/src/main/res/drawable/circle.xml b/reactions/src/main/res/drawable/circle.xml new file mode 100644 index 00000000..853d7c36 --- /dev/null +++ b/reactions/src/main/res/drawable/circle.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/reactions/src/main/res/layout/activity_emoji_reaction_picker.xml b/reactions/src/main/res/layout/activity_emoji_reaction_picker.xml index 3a61d885..5419f266 100644 --- a/reactions/src/main/res/layout/activity_emoji_reaction_picker.xml +++ b/reactions/src/main/res/layout/activity_emoji_reaction_picker.xml @@ -57,7 +57,7 @@ diff --git a/reactions/src/main/res/layout/grid_item_emoji.xml b/reactions/src/main/res/layout/grid_item_emoji.xml index ddd6f4c8..f1ff8474 100644 --- a/reactions/src/main/res/layout/grid_item_emoji.xml +++ b/reactions/src/main/res/layout/grid_item_emoji.xml @@ -5,6 +5,16 @@ android:layout_height="40dp" tools:showIn="@layout/activity_emoji_reaction_picker"> + + + + #f2f5f8 + #4ac1c9d6 + diff --git a/reactions/src/main/res/values/font_certs.xml b/reactions/src/main/res/values/font_certs.xml new file mode 100644 index 00000000..141bfc01 --- /dev/null +++ b/reactions/src/main/res/values/font_certs.xml @@ -0,0 +1,17 @@ + + + + @array/com_google_android_gms_fonts_certs_dev + @array/com_google_android_gms_fonts_certs_prod + + + + MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= + + + + + MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK + + + \ No newline at end of file diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 7aac94fa..69e6a7b2 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -43,6 +43,16 @@ android:name=".core.services.CallService" android:exported="false" /> + + + + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorViewModel.kt index 0793a931..a74ab33c 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorViewModel.kt @@ -18,6 +18,7 @@ package im.vector.riotredesign.core.platform import com.airbnb.mvrx.BaseMvRxViewModel import com.airbnb.mvrx.MvRxState +import im.vector.riotredesign.BuildConfig abstract class VectorViewModel(initialState: S) - : BaseMvRxViewModel(initialState, debugMode = false) \ No newline at end of file + : BaseMvRxViewModel(initialState, debugMode = BuildConfig.DEBUG) \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/ExternalApplicationsUtil.kt index 318549b2..00fe3fd9 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/ExternalApplicationsUtil.kt @@ -238,3 +238,26 @@ fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) { activity.toast(R.string.error_no_external_application_found) } } + +fun shareMedia(context: Context, file: File, mediaMimeType: String?) { + + var mediaUri: Uri? = null + try { + mediaUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", file) + } catch (e: Exception) { + Timber.e("onMediaAction Selected File cannot be shared " + e.message) + } + + + if (null != mediaUri) { + val sendIntent = Intent() + sendIntent.action = Intent.ACTION_SEND + // Grant temporary read permission to the content URI + sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + sendIntent.type = mediaMimeType + sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri) + + context.startActivity(sendIntent) + + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/SystemUtils.kt index d21f0d7c..5c8e3c57 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/SystemUtils.kt @@ -76,10 +76,12 @@ fun requestDisablingBatteryOptimization(activity: Activity, fragment: Fragment?, * @param context the context * @param text the text to copy */ -fun copyToClipboard(context: Context, text: CharSequence) { +fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = true) { val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboard.primaryClip = ClipData.newPlainText("", text) - context.toast(R.string.copied_to_clipboard) + if (showToast) { + context.toast(R.string.copied_to_clipboard) + } } /** diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt index 7d385006..b1cd3f39 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt @@ -33,7 +33,7 @@ import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository import io.reactivex.rxkotlin.subscribeBy import org.koin.android.ext.android.get -class EmptyState : MvRxState +data class EmptyState(val isEmpty: Boolean = true) : MvRxState class HomeActivityViewModel(state: EmptyState, private val session: Session, 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 094cf225..9162ced3 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 @@ -20,16 +20,28 @@ import android.app.Activity.RESULT_OK import android.content.Intent import android.graphics.Color import android.graphics.drawable.ColorDrawable +import android.net.Uri import android.os.Bundle import android.os.Parcelable import android.text.Editable import android.text.Spannable +import android.view.HapticFeedbackConstants +import android.view.LayoutInflater import android.view.View +import android.widget.TextView +import android.widget.Toast import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyVisibilityTracker +import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.fragmentViewModel +import com.github.piasy.biv.BigImageViewer +import com.github.piasy.biv.loader.ImageLoader +import com.google.android.material.snackbar.Snackbar import com.jaiselrahman.filepicker.activity.FilePickerActivity import com.jaiselrahman.filepicker.config.Configurations import com.jaiselrahman.filepicker.model.MediaFile @@ -41,8 +53,10 @@ import im.vector.matrix.android.api.session.room.model.message.MessageAudioConte import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.user.model.User +import im.vector.reactions.EmojiReactionPickerActivity import im.vector.riotredesign.R import im.vector.riotredesign.core.dialogs.DialogListItem import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer @@ -62,7 +76,11 @@ import im.vector.riotredesign.features.home.room.detail.composer.TextComposerAct import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController +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.helper.EndlessRecyclerViewScrollListener +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotredesign.features.html.PillImageSpan import im.vector.riotredesign.features.media.ImageContentRenderer import im.vector.riotredesign.features.media.ImageMediaViewerActivity @@ -75,6 +93,7 @@ import org.koin.android.scope.ext.android.bindScope import org.koin.android.scope.ext.android.getOrCreateScope import org.koin.core.parameter.parametersOf import timber.log.Timber +import java.io.File @Parcelize @@ -88,7 +107,10 @@ private const val CAMERA_VALUE_TITLE = "attachment" private const val REQUEST_FILES_REQUEST_CODE = 0 private const val TAKE_IMAGE_REQUEST_CODE = 1 -class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback { +class RoomDetailFragment : + VectorBaseFragment(), + TimelineEventController.Callback, + AutocompleteUserPresenter.Callback { companion object { @@ -115,8 +137,11 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac override fun getLayoutResId() = R.layout.fragment_room_detail + lateinit var actionViewModel: ActionsHandler + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) + actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE)) setupRecyclerView() setupToolbar() @@ -125,6 +150,10 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac roomDetailViewModel.subscribe { renderState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } + + actionViewModel.actionCommandEvent.observe(this, Observer { + handleActions(it) + }) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -136,7 +165,6 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac } } - override fun onResume() { super.onResume() roomDetailViewModel.process(RoomDetailActions.IsDisplayed) @@ -402,8 +430,87 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac } // AutocompleteUserPresenter.Callback + override fun onEventLongClicked(eventId: String, informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean { + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + val roomId = (arguments?.get(MvRx.KEY_ARG) as? RoomDetailArgs)?.roomId + if (roomId.isNullOrBlank()) { + Timber.e("Missing RoomId, cannot open bottomsheet") + return false + } + MessageActionsBottomSheet + .newInstance(eventId, roomId, informationData) + .show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS") + return true + } + + +// AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) } + + private fun handleActions(it: LiveEvent?) { + it?.getContentIfNotHandled()?.let { actionData -> + + when (actionData.actionId) { + MessageMenuViewModel.ACTION_ADD_REACTION -> { + startActivityForResult(EmojiReactionPickerActivity.intent(requireContext()), 0) + } + 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() + } + MessageMenuViewModel.ACTION_SHARE -> { + //TODO current data communication is too limited + //Need to now the media type + actionData.data?.toString()?.let { + //TODO bad, just POC + BigImageViewer.imageLoader().loadImage( + actionData.hashCode(), + Uri.parse(it), + object : ImageLoader.Callback { + override fun onFinish() {} + + override fun onSuccess(image: File?) { + if (image != null) + shareMedia(requireContext(), image!!, "image/*") + } + + override fun onFail(error: Exception?) {} + + override fun onCacheHit(imageType: Int, image: File?) {} + + override fun onCacheMiss(imageType: Int, image: File?) {} + + override fun onProgress(progress: Int) {} + + override fun onStart() {} + + } + + ) + } + } + MessageMenuViewModel.VIEW_SOURCE, + MessageMenuViewModel.VIEW_DECRYPTED_SOURCE -> { + val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) + view.findViewById(R.id.event_content_text_view)?.let { + it.text = actionData.data?.toString() ?: "" + } + + AlertDialog.Builder(requireActivity()) + .setView(view) + .setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } + .show() + } + 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/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 05b20b2f..8533eb8c 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 @@ -26,27 +26,17 @@ import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel 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.MessageAudioContent -import im.vector.matrix.android.api.session.room.model.message.MessageFileContent -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.core.epoxy.LoadingItemModel_ import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory -import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineAsyncHelper -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.riotredesign.features.home.room.detail.timeline.helper.canBeMerged -import im.vector.riotredesign.features.home.room.detail.timeline.helper.nextDisplayableEvent -import im.vector.riotredesign.features.home.room.detail.timeline.helper.prevSameTypeEvents +import im.vector.riotredesign.features.home.room.detail.timeline.helper.* import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.riotredesign.features.home.room.detail.timeline.item.MergedHeaderItem +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotredesign.features.media.ImageContentRenderer import im.vector.riotredesign.features.media.VideoContentRenderer import org.threeten.bp.LocalDateTime @@ -64,6 +54,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) fun onFileMessageClicked(messageFileContent: MessageFileContent) fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) + fun onEventLongClicked(eventId: String, informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean } private val collapsedEventIds = linkedSetOf() @@ -170,8 +161,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -245,7 +236,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, // => handle case where paginating from mergeable events and we get more val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) - ?: true + ?: true val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } if (isCollapsed) { collapsedEventIds.addAll(mergedEventIds) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ActionsHandler.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ActionsHandler.kt new file mode 100644 index 00000000..4b876a49 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/ActionsHandler.kt @@ -0,0 +1,23 @@ +package im.vector.riotredesign.features.home.room.detail.timeline.action + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import im.vector.riotredesign.core.utils.LiveEvent + +/** + * Activity shared view model to handle message actions + */ +class ActionsHandler : ViewModel() { + + data class ActionData( + val actionId: String, + val data: Any? + ) + + val actionCommandEvent = MutableLiveData>() + + fun fireAction(actionId: String, data: Any? = null) { + actionCommandEvent.value = LiveEvent(ActionData(actionId,data)) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/BaseMvRxBottomSheetDialog.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/BaseMvRxBottomSheetDialog.kt new file mode 100644 index 00000000..7001d730 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/BaseMvRxBottomSheetDialog.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.home.room.detail.timeline.action + +import android.os.Bundle +import com.airbnb.mvrx.MvRxView +import com.airbnb.mvrx.MvRxViewModelStore +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + + +abstract class BaseMvRxBottomSheetDialog() : BottomSheetDialogFragment(), MvRxView { + override val mvrxViewModelStore by lazy { MvRxViewModelStore(viewModelStore) } + + override fun onCreate(savedInstanceState: Bundle?) { + mvrxViewModelStore.restoreViewModels(this, savedInstanceState) + super.onCreate(savedInstanceState) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + mvrxViewModelStore.saveViewModels(outState) + } + + override fun onStart() { + super.onStart() + // This ensures that invalidate() is called for static screens that don't + // subscribe to a ViewModel. + postInvalidate() + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..b2d4b377 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.home.room.detail.timeline.action + +import android.app.Dialog +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.lifecycle.ViewModelProviders +import butterknife.BindView +import butterknife.ButterKnife +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import im.vector.riotredesign.R +import im.vector.riotredesign.core.glide.GlideApp +import im.vector.riotredesign.features.home.AvatarRenderer +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData +import kotlinx.android.parcel.Parcelize + + +class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() { + + private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class) + + private lateinit var actionHandlerModel: ActionsHandler + + @BindView(R.id.bottom_sheet_message_preview_avatar) + lateinit var senderAvatarImageView: ImageView + + @BindView(R.id.bottom_sheet_message_preview_sender) + lateinit var senderNameTextView: TextView + + @BindView(R.id.bottom_sheet_message_preview_body) + lateinit var messageBodyTextView: TextView + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.bottom_sheet_message_actions, container, false) + ButterKnife.bind(this, view) + return view + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java) + + val cfm = childFragmentManager + var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment + if (menuActionFragment == null) { + menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as ParcelableArgs) + cfm.beginTransaction() + .replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment") + .commit() + } + menuActionFragment.interactionListener = object : MessageMenuFragment.InteractionListener { + override fun didSelectMenuAction(simpleAction: SimpleAction) { + actionHandlerModel.fireAction(simpleAction.uid, simpleAction.data) + dismiss() + } + } + + + var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment + if (quickReactionFragment == null) { + quickReactionFragment = QuickReactionFragment.newInstance() + cfm.beginTransaction() + .replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction") + .commit() + } + quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener { + override fun didQuickReactWith(reactions: List) { + actionHandlerModel.fireAction("Quick React", reactions) + dismiss() + } + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + //We want to force the bottom sheet initial state to expanded + (dialog as? BottomSheetDialog)?.let { bottomSheetDialog -> + bottomSheetDialog.setOnShowListener { dialog -> + val d = dialog as BottomSheetDialog + (d.findViewById(com.google.android.material.R.id.design_bottom_sheet) as? FrameLayout)?.let { + BottomSheetBehavior.from(it).state = BottomSheetBehavior.STATE_COLLAPSED + } + } + } + return dialog + } + + override fun invalidate() = withState(viewModel) { + senderNameTextView.text = it.senderName + messageBodyTextView.text = it.messageBody + + 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)) + } + return@withState + } + + + @Parcelize + data class ParcelableArgs( + val eventId: String, + val roomId: String, + val informationData: MessageInformationData +// val body: String, +// val type: String, +// var url: String? = null + ) : Parcelable + + companion object { + fun newInstance(eventId: String, roomId: String, informationData: MessageInformationData): MessageActionsBottomSheet { + val args = Bundle() + val parcelableArgs = ParcelableArgs( + eventId, + roomId, + informationData +// messageContent.body, +// messageContent.type + ) +// if (messageContent is MessageImageContent) { +// parcelableArgs.url = messageContent.url +// } +// if (messageContent is MessageVideoContent) { +// parcelableArgs.url = messageContent.url +// } + + args.putParcelable(MvRx.KEY_ARG, parcelableArgs) + return MessageActionsBottomSheet().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/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt new file mode 100644 index 00000000..7e08effe --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -0,0 +1,53 @@ +package im.vector.riotredesign.features.home.room.detail.timeline.action + +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.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.riotredesign.core.platform.VectorViewModel +import org.koin.android.ext.android.get +import timber.log.Timber + + +data class MessageActionState( + val userId: String, + val senderName: String, + val messageBody: String, + val senderAvatarPath: String? = null) + : MvRxState + + +class MessageActionsViewModel(initialState: MessageActionState) : VectorViewModel(initialState) { + + companion object : MvRxViewModelFactory { + +// override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { +// //val currentSession = viewModelContext.activity.get() +// return MessageActionsViewModel(state/*,currentSession*/) +// } + + override fun initialState(viewModelContext: ViewModelContext): MessageActionState? { + val currentSession = viewModelContext.activity.get() + val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs + + + val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId) + return if (event != null) { + val messageContent: MessageContent? = event.root.content.toModel() + + MessageActionState( + event.root.sender ?: "", + parcel.informationData.memberName.toString(), + messageContent?.body ?: "", + currentSession.contentUrlResolver().resolveFullSize(parcel.informationData.avatarUrl) + ) + } else { + //can this happen? + Timber.e("Failed to retrieve event") + null + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..ae3eda31 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuFragment.kt @@ -0,0 +1,96 @@ +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.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.airbnb.mvrx.BaseMvRxFragment +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.riotredesign.R +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotredesign.features.themes.ThemeUtils + +class MessageMenuFragment : BaseMvRxFragment() { + + private val viewModel: MessageMenuViewModel by fragmentViewModel(MessageMenuViewModel::class) + + private var addSeparators = false + + var interactionListener: InteractionListener? = null + + override fun invalidate() = withState(viewModel) { state -> + + val linearLayout = view as? LinearLayout + if (linearLayout != null) { + val inflater = LayoutInflater.from(linearLayout.context) + linearLayout.removeAllViews() + var insertIndex = 0 + state.actions.forEachIndexed { index, action -> + inflateActionView(action, inflater, linearLayout)?.let { + it.setOnClickListener { + interactionListener?.didSelectMenuAction(action) + } + linearLayout.addView(it, insertIndex) + insertIndex++ + if (addSeparators) { + if (index < state.actions.size - 1) { + linearLayout.addView(inflateSeparatorView(), insertIndex) + insertIndex++ + } + } + } + } + } + + } + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + //we just create programmatically + val contentView = LinearLayout(context) + contentView.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + contentView.orientation = LinearLayout.VERTICAL + return contentView + } + + private fun inflateActionView(action: SimpleAction, inflater: LayoutInflater, container: ViewGroup?): View? { + return inflater.inflate(R.layout.adapter_item_action, container, false)?.apply { + if (action.iconResId != null) { + findViewById(R.id.action_icon)?.setImageResource(action.iconResId) + } else { + findViewById(R.id.action_icon)?.setImageDrawable(null) + } + findViewById(R.id.action_title)?.setText(action.titleRes) + } + } + + private fun inflateSeparatorView(): View { + val frame = FrameLayout(context) + frame.setBackgroundColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_list_divider_color)) + frame.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, requireContext().resources.displayMetrics.density.toInt()) + return frame + + } + + interface InteractionListener { + fun didSelectMenuAction(simpleAction: SimpleAction) + } + + + companion object { + fun newInstance(pa: MessageActionsBottomSheet.ParcelableArgs): MessageMenuFragment { + val args = Bundle() + args.putParcelable(MvRx.KEY_ARG, pa) + val fragment = MessageMenuFragment() + fragment.arguments = args + return fragment + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..6c975809 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -0,0 +1,165 @@ +package im.vector.riotredesign.features.home.room.detail.timeline.action + +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.toContent +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.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.VectorViewModel +import org.json.JSONObject +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 + +class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel(initialState) { + + companion object : MvRxViewModelFactory { + + override fun initialState(viewModelContext: ViewModelContext): MessageMenuState? { + // Args are accessible from the context. + val currentSession = viewModelContext.activity.get() + val parcel = viewModelContext.args as MessageActionsBottomSheet.ParcelableArgs + val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId) + ?: return null + + val messageContent: MessageContent = event.root.content.toModel() ?: return null + val type = messageContent.type + + if (event.sendState == SendState.UNSENT) { + //Resend and Delete + return MessageMenuState( + listOf( + SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_corner_down_right, event.root.eventId), + //TODO delete icon + SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_material_delete, event.root.eventId) + ) + ) + } + + + //TODO determine if can copy, forward, reply, quote, report? + val actions = ArrayList().apply { + this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_smile)) + if (canCopy(type)) { + //TODO copy images? html? see ClipBoard + this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent.body)) + } + + if (canQuote(event, messageContent)) { + //TODO quote icon + this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_material_quote, parcel.eventId)) + } + + if (canReply(event, messageContent)) { + this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_corner_down_right)) + } + if (canShare(type)) { + if (messageContent is MessageImageContent) { + this.add(SimpleAction(ACTION_SHARE, R.string.share, R.drawable.ic_share, currentSession.contentUrlResolver().resolveFullSize(messageContent.url))) + } + //TODO + } + + //TODO is uploading + //TODO is downloading + + if (event.sendState == SendState.SENT) { + + //TODO Can be redacted + + //TODO sent by me or sufficient power level + } + + + this.add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, JSONObject(event.root.toContent()).toString(4))) + if (event.isEncrypted()) { + this.add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, null, parcel.eventId)) + } + this.add(SimpleAction(PERMALINK, R.string.permalink, R.drawable.ic_permalink, parcel.eventId)) + } + + return MessageMenuState(actions) + } + + private fun canReply(event: TimelineEvent, messageContent: MessageContent): Boolean { + //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment + if (event.root.type != EventType.MESSAGE) return false + return when (messageContent.type) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_FILE -> true + else -> false + } + } + + private fun canQuote(event: TimelineEvent, messageContent: MessageContent): Boolean { + //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment + if (event.root.type != EventType.MESSAGE) return false + return when (messageContent.type) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.FORMAT_MATRIX_HTML, + MessageType.MSGTYPE_LOCATION -> { + true + } + else -> false + } + } + + private fun canCopy(type: String): Boolean { + return when (type) { + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE, + MessageType.MSGTYPE_EMOTE, + MessageType.FORMAT_MATRIX_HTML, + MessageType.MSGTYPE_LOCATION -> { + true + } + else -> false + } + } + + + private fun canShare(type: String): Boolean { + return when (type) { + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_VIDEO -> { + true + } + else -> false + } + } + + const val ACTION_ADD_REACTION = "add_reaction" + const val ACTION_COPY = "copy" + const val ACTION_QUOTE = "quote" + const val ACTION_REPLY = "reply" + const val ACTION_SHARE = "share" + const val ACTION_RESEND = "resend" + const val ACTION_DELETE = "delete" + const val VIEW_SOURCE = "VIEW_SOURCE" + const val VIEW_DECRYPTED_SOURCE = "VIEW_DECRYPTED_SOURCE" + const val PERMALINK = "PERMALINK" + + + } +} 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 new file mode 100644 index 00000000..838083bb --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionFragment.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.home.room.detail.timeline.action + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.transition.TransitionManager +import butterknife.BindView +import butterknife.ButterKnife +import com.airbnb.mvrx.BaseMvRxFragment +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotredesign.R + + +class QuickReactionFragment : BaseMvRxFragment() { + + private val viewModel: QuickReactionViewModel by fragmentViewModel(QuickReactionViewModel::class) + + + @BindView(R.id.root_layout) + lateinit var rootLayout: ConstraintLayout + + + @BindView(R.id.quick_react_1) + lateinit var quickReact1: View + @BindView(R.id.quick_react_2) + lateinit var quickReact2: View + @BindView(R.id.quick_react_3) + lateinit var quickReact3: View + @BindView(R.id.quick_react_4) + lateinit var quickReact4: View + + + @BindView(R.id.quick_react_1_text) + lateinit var quickReact1Text: TextView + + @BindView(R.id.quick_react_2_text) + lateinit var quickReact2Text: TextView + + @BindView(R.id.quick_react_3_text) + lateinit var quickReact3Text: TextView + + @BindView(R.id.quick_react_4_text) + lateinit var quickReact4Text: TextView + + var interactionListener: InteractionListener? = null + + 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) + return view + } + + override fun onAttach(context: Context) { + super.onAttach(context) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + quickReact1Text.text = viewModel.agreePositive + quickReact2Text.text = viewModel.agreeNegative + quickReact3Text.text = viewModel.likePositive + quickReact4Text.text = viewModel.likeNegative + + //configure click listeners + quickReact1.setOnClickListener { + viewModel.toggleAgree(true) + } + quickReact2.setOnClickListener { + viewModel.toggleAgree(false) + } + quickReact3.setOnClickListener { + viewModel.toggleLike(true) + } + quickReact4.setOnClickListener { + viewModel.toggleLike(false) + } + + } + + override fun invalidate() = withState(viewModel) { + + TransitionManager.beginDelayedTransition(rootLayout) + when (it.agreeTrigleState) { + TriggleState.NONE -> { + quickReact1.alpha = 1f + quickReact2.alpha = 1f + } + TriggleState.FIRST -> { + quickReact1.alpha = 1f + quickReact2.alpha = 0.2f + + } + TriggleState.SECOND -> { + quickReact1.alpha = 0.2f + quickReact2.alpha = 1f + } + } + when (it.likeTriggleState) { + TriggleState.NONE -> { + quickReact3.alpha = 1f + quickReact4.alpha = 1f + } + TriggleState.FIRST -> { + quickReact3.alpha = 1f + quickReact4.alpha = 0.2f + + } + TriggleState.SECOND -> { + quickReact3.alpha = 0.2f + quickReact4.alpha = 1f + } + } + + if (it.selectionResult != null) { + interactionListener?.didQuickReactWith(it.selectionResult) + } + } + + interface InteractionListener { + fun didQuickReactWith(reactions: List) + } + + companion object { + fun newInstance(): QuickReactionFragment { + return QuickReactionFragment() + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..73126b3b --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.home.room.detail.timeline.action + +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import im.vector.riotredesign.core.platform.VectorViewModel + +/** + * Quick reactions state, it's a toggle with 3rd state + */ +enum class TriggleState { + NONE, + FIRST, + SECOND +} + +data class QuickReactionState(val agreeTrigleState: TriggleState, val likeTriggleState: TriggleState, val selectionResult: List? = null) : MvRxState + +class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel(initialState) { + + val agreePositive = "👍" + val agreeNegative = "👎" + val likePositive = "😀" + val likeNegative = "😞" + + + fun toggleAgree(isFirst: Boolean) = withState { + if (isFirst) { + setState { + copy( + agreeTrigleState = if (it.agreeTrigleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST, + selectionResult = getReactions(this) + ) + } + } else { + setState { + copy( + agreeTrigleState = if (it.agreeTrigleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND, + selectionResult = getReactions(this) + ) + } + } + } + + fun toggleLike(isFirst: Boolean) = withState { + if (isFirst) { + setState { + copy( + likeTriggleState = if (it.likeTriggleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST, + selectionResult = getReactions(this) + ) + } + } else { + setState { + copy( + likeTriggleState = if (it.likeTriggleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND, + selectionResult = getReactions(this) + ) + } + } + } + + private fun getReactions(state: QuickReactionState): List { + return ArrayList(4).apply { + when (state.likeTriggleState) { + TriggleState.FIRST -> add(likePositive) + TriggleState.SECOND -> add(likeNegative) + } + when (state.agreeTrigleState) { + TriggleState.FIRST -> add(agreePositive) + TriggleState.SECOND -> add(agreeNegative) + } + } + } + + + companion object : MvRxViewModelFactory { + + override fun initialState(viewModelContext: ViewModelContext): QuickReactionState? { + // Args are accessible from the context. + // val foo = vieWModelContext.args.foo + return QuickReactionState(TriggleState.NONE, TriggleState.NONE) + } + } +} \ No newline at end of file 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 f17cc295..529cf91c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -23,14 +23,8 @@ import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent -import im.vector.matrix.android.api.session.room.model.message.MessageFileContent -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent -import im.vector.matrix.android.api.session.room.model.message.MessageTextContent -import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +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.R import im.vector.riotredesign.core.epoxy.VectorEpoxyModel @@ -40,15 +34,7 @@ import im.vector.riotredesign.core.resources.ColorProvider import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem -import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem_ -import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageFileItem -import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageFileItem_ -import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem -import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem_ -import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem -import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem_ +import im.vector.riotredesign.features.home.room.detail.timeline.item.* import im.vector.riotredesign.features.html.EventHtmlRenderer import im.vector.riotredesign.features.media.ImageContentRenderer import im.vector.riotredesign.features.media.VideoContentRenderer @@ -70,13 +56,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider, val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60)) - ?: false + ?: false val showInformation = addDaySeparator - || event.senderAvatar != nextEvent?.senderAvatar - || event.senderName != nextEvent?.senderName - || nextEvent?.root?.type != EventType.MESSAGE - || isNextMessageReceivedMoreThanOneHourAgo + || event.senderAvatar != nextEvent?.senderAvatar + || event.senderName != nextEvent?.senderName + || nextEvent?.root?.type != EventType.MESSAGE + || isNextMessageReceivedMoreThanOneHourAgo val messageContent: MessageContent = event.root.content.toModel() ?: return null val time = timelineDateFormatter.formatMessageHour(date) @@ -86,22 +72,24 @@ class MessageItemFactory(private val colorProvider: ColorProvider, textColor = colorProvider.getColor(getColorFor(event.root.sender ?: "")) } val informationData = MessageInformationData(eventId = eventId, - senderId = event.root.sender ?: "", - sendState = event.sendState, - time = time, - avatarUrl = avatarUrl, - memberName = formattedMemberName, - showInformation = showInformation) + senderId = event.root.sender ?: "", + sendState = event.sendState, + time = time, + avatarUrl = avatarUrl, + memberName = formattedMemberName, + showInformation = showInformation) +// val all = event.root.toContent() +// val ev = all.toModel() return when (messageContent) { - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback) - is MessageTextContent -> buildTextMessageItem(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 MessageEmoteContent -> buildEmoteMessageItem(eventId, messageContent, informationData, callback) + is MessageTextContent -> buildTextMessageItem(eventId, event.sendState, messageContent, informationData, callback) + is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback) + is MessageNoticeContent -> buildNoticeMessageItem(eventId, messageContent, informationData, callback) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback) + is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback) + is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback) + else -> buildNotHandledMessageItem(messageContent) } } @@ -179,7 +167,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider, .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } } - private fun buildTextMessageItem(messageContent: MessageTextContent, + private fun buildTextMessageItem(eventId: String, sendState: SendState, + messageContent: MessageTextContent, informationData: MessageInformationData, callback: TimelineEventController.Callback?): MessageTextItem? { @@ -191,9 +180,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageTextItem_() .message(linkifiedBody) .informationData(informationData) + .longClickListener { view -> + return@longClickListener callback?.onEventLongClicked(eventId, informationData, messageContent, view) + ?: false + } } - private fun buildNoticeMessageItem(messageContent: MessageNoticeContent, + private fun buildNoticeMessageItem(eventId: String, messageContent: MessageNoticeContent, informationData: MessageInformationData, callback: TimelineEventController.Callback?): MessageTextItem? { @@ -208,9 +201,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageTextItem_() .message(message) .informationData(informationData) + .longClickListener { view -> + return@longClickListener callback?.onEventLongClicked(eventId, informationData, messageContent, view) + ?: false + } } - private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, + private fun buildEmoteMessageItem(eventId: String, messageContent: MessageEmoteContent, informationData: MessageInformationData, callback: TimelineEventController.Callback?): MessageTextItem? { @@ -221,6 +218,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageTextItem_() .message(message) .informationData(informationData) + .longClickListener { view -> + return@longClickListener callback?.onEventLongClicked(eventId, informationData, messageContent, view) + ?: false + } } private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): Spannable { @@ -251,13 +252,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider, } val cI = Math.abs(hash) % 8 + 1 return when (cI) { - 1 -> R.color.username_1 - 2 -> R.color.username_2 - 3 -> R.color.username_3 - 4 -> R.color.username_4 - 5 -> R.color.username_5 - 6 -> R.color.username_6 - 7 -> R.color.username_7 + 1 -> R.color.username_1 + 2 -> R.color.username_2 + 3 -> R.color.username_3 + 4 -> R.color.username_4 + 5 -> R.color.username_5 + 6 -> R.color.username_6 + 7 -> R.color.username_7 else -> R.color.username_8 } } 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 17c2ea4a..48687c78 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 @@ -19,6 +19,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageView import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.features.home.AvatarRenderer @@ -27,6 +28,9 @@ abstract class AbsMessageItem : VectorEpoxyModel() abstract val informationData: MessageInformationData + @EpoxyAttribute + var longClickListener: View.OnLongClickListener? = null + override fun bind(holder: H) { super.bind(holder) if (informationData.showInformation) { @@ -41,6 +45,7 @@ abstract class AbsMessageItem : VectorEpoxyModel() holder.memberNameView.visibility = View.GONE holder.timeView.visibility = View.GONE } + holder.view.setOnLongClickListener(longClickListener) } protected fun View.renderSendState() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt index 286f11a5..cf763803 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -18,6 +18,10 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item import im.vector.matrix.android.api.session.room.send.SendState +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class MessageInformationData( val eventId: String, val senderId: String, @@ -26,4 +30,4 @@ data class MessageInformationData( val avatarUrl: String?, val memberName: CharSequence? = null, val showInformation: Boolean = true -) \ No newline at end of file +) : Parcelable \ No newline at end of file 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 45ba0ec1..58721a17 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 @@ -46,6 +46,7 @@ abstract class MessageTextItem : AbsMessageItem() { null) holder.messageView.setTextFuture(textFuture) holder.messageView.renderSendState() + holder.messageView.setOnLongClickListener(longClickListener) findPillsAndProcess { it.bind(holder.messageView) } } 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 82156153..3aa606eb 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 @@ -16,6 +16,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item +import android.view.View import android.widget.ImageView import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute @@ -33,9 +34,14 @@ abstract class NoticeItem : VectorEpoxyModel() { @EpoxyAttribute var userId: String = "" @EpoxyAttribute var memberName: CharSequence? = null + + @EpoxyAttribute + var longClickListener: View.OnLongClickListener? = null + override fun bind(holder: Holder) { holder.noticeTextView.text = noticeText AvatarRenderer.render(avatarUrl, userId, memberName?.toString(), holder.avatarImageView) + holder.view.setOnLongClickListener(longClickListener) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/riotredesign/features/themes/ThemeUtils.kt index 06b3e47c..8550c3c1 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/themes/ThemeUtils.kt @@ -160,6 +160,17 @@ object ThemeUtils { return matchedColor } + fun getAttribute(c: Context, @AttrRes attribute: Int): TypedValue? { + try { + val typedValue = TypedValue() + c.theme.resolveAttribute(attribute, typedValue, true) + return typedValue + } catch (e: Exception) { + Timber.e(e, "Unable to get color") + } + return null + } + /** * Get the resource Id applied to the current theme * diff --git a/vector/src/main/res/drawable/ic_copy.xml b/vector/src/main/res/drawable/ic_copy.xml new file mode 100644 index 00000000..3eef35b7 --- /dev/null +++ b/vector/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,11 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_corner_down_right.xml b/vector/src/main/res/drawable/ic_corner_down_right.xml new file mode 100644 index 00000000..109222a3 --- /dev/null +++ b/vector/src/main/res/drawable/ic_corner_down_right.xml @@ -0,0 +1,22 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_edit.xml b/vector/src/main/res/drawable/ic_edit.xml new file mode 100644 index 00000000..ec5cf418 --- /dev/null +++ b/vector/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,22 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_more_horizontal.xml b/vector/src/main/res/drawable/ic_more_horizontal.xml new file mode 100644 index 00000000..3aab2fad --- /dev/null +++ b/vector/src/main/res/drawable/ic_more_horizontal.xml @@ -0,0 +1,30 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_more_horizontal_2.xml b/vector/src/main/res/drawable/ic_more_horizontal_2.xml new file mode 100644 index 00000000..3aab2fad --- /dev/null +++ b/vector/src/main/res/drawable/ic_more_horizontal_2.xml @@ -0,0 +1,30 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_permalink.xml b/vector/src/main/res/drawable/ic_permalink.xml new file mode 100644 index 00000000..f8c74537 --- /dev/null +++ b/vector/src/main/res/drawable/ic_permalink.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_share.xml b/vector/src/main/res/drawable/ic_share.xml new file mode 100644 index 00000000..0be45704 --- /dev/null +++ b/vector/src/main/res/drawable/ic_share.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/drawable/ic_smile.xml b/vector/src/main/res/drawable/ic_smile.xml new file mode 100644 index 00000000..e2f3402e --- /dev/null +++ b/vector/src/main/res/drawable/ic_smile.xml @@ -0,0 +1,34 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_view_source.xml b/vector/src/main/res/drawable/ic_view_source.xml new file mode 100644 index 00000000..f139832b --- /dev/null +++ b/vector/src/main/res/drawable/ic_view_source.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/vector/src/main/res/layout/activity_home.xml b/vector/src/main/res/layout/activity_home.xml index 39aa8784..401ab626 100644 --- a/vector/src/main/res/layout/activity_home.xml +++ b/vector/src/main/res/layout/activity_home.xml @@ -6,10 +6,16 @@ android:layout_height="match_parent" tools:openDrawer="start"> - + android:layout_height="match_parent"> + + + + + + + + + + diff --git a/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml b/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml new file mode 100644 index 00000000..6bdcc7dc --- /dev/null +++ b/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/bottom_sheet_message_actions.xml b/vector/src/main/res/layout/bottom_sheet_message_actions.xml new file mode 100644 index 00000000..f148068f --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_message_actions.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/dialog_event_content.xml b/vector/src/main/res/layout/dialog_event_content.xml new file mode 100644 index 00000000..a3bcad89 --- /dev/null +++ b/vector/src/main/res/layout/dialog_event_content.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_text_message.xml b/vector/src/main/res/layout/item_timeline_event_text_message.xml index bd6f9f89..b60cce8f 100644 --- a/vector/src/main/res/layout/item_timeline_event_text_message.xml +++ b/vector/src/main/res/layout/item_timeline_event_text_message.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" android:paddingLeft="16dp" android:paddingRight="16dp"> @@ -54,6 +55,7 @@ android:id="@+id/messageTextView" android:layout_width="0dp" android:layout_height="wrap_content" + android:foreground="?attr/selectableItemBackgroundBorderless" android:layout_marginStart="64dp" android:layout_marginLeft="64dp" android:layout_marginBottom="8dp" diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 5758dcf3..4d351e8f 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -57,6 +57,8 @@ Stay Send Copy + Edit + Reply Resend Remove Quote @@ -1399,5 +1401,10 @@ Why choose Riot.im? "Autocomplete Server Options Riot detected a custom server configuration for your userId domain \"%s\":\n%s Use Config + + + Agree + Like + Add Reaction diff --git a/vector/src/main/res/xml/riotx_provider_paths.xml b/vector/src/main/res/xml/riotx_provider_paths.xml new file mode 100644 index 00000000..7d3fcb22 --- /dev/null +++ b/vector/src/main/res/xml/riotx_provider_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file