[WIP] Emoji Reactions

This commit is contained in:
Valere 2019-05-07 14:02:15 +02:00
parent a64f509872
commit 56a2a3a065
60 changed files with 1985 additions and 194 deletions

View File

@ -8,7 +8,8 @@ buildscript {
jcenter() jcenter()
maven { maven {
url "https://plugins.gradle.org/m2/" url "https://plugins.gradle.org/m2/"
} } }
}
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.3.2' classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.google.gms:google-services:4.2.0' classpath 'com.google.gms:google-services:4.2.0'

View File

@ -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
}

View File

@ -16,6 +16,7 @@


package im.vector.matrix.android.api.session.room.model.message package im.vector.matrix.android.api.session.room.model.message



interface MessageContent { interface MessageContent {
val type: String val type: String
val body: String val body: String

View File

@ -18,9 +18,13 @@ package im.vector.matrix.android.api.session.room.send


enum class SendState { enum class SendState {
UNKNOWN, UNKNOWN,
// the event has not been sent
UNSENT, UNSENT,
// the event is encrypting
ENCRYPTING, ENCRYPTING,
// the event is currently sending
SENDING, SENDING,
// the event has been sent
SENT, SENT,
SYNCED; SYNCED;



View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.room.timeline 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.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.model.RoomMember
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState


@ -59,4 +60,8 @@ data class TimelineEvent(
inline fun <reified T> getMetadata(key: String): T? { inline fun <reified T> getMetadata(key: String): T? {
return metadata[key] as T? return metadata[key] as T?
} }

fun isEncrypted() : Boolean {
return EventType.ENCRYPTED == root.type
}
} }

View File

@ -30,4 +30,6 @@ interface TimelineService {
*/ */
fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline



fun getTimeLineEvent(eventId: String): TimelineEvent?
} }

View File

@ -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.next
import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.prev
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmQuery import io.realm.RealmQuery


internal class SenderRoomMemberExtractor(private val roomId: String) { 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 val sender = event.sender ?: return null
// If the event is unlinked we want to fetch unlinked state events // If the event is unlinked we want to fetch unlinked state events
val unlinked = event.isUnlinked val unlinked = event.isUnlinked
val roomEntity = RoomEntity.where(event.realm, roomId = roomId).findFirst() ?: return null val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return null
val chunkEntity = ChunkEntity.findIncludingEvent(event.realm, event.eventId) val chunkEntity = ChunkEntity.findIncludingEvent(realm, event.eventId)
val content = when { val content = when {
chunkEntity == null -> null chunkEntity == null -> null
event.stateIndex <= 0 -> baseQuery(chunkEntity.events, sender, unlinked).next(from = event.stateIndex)?.prevContent event.stateIndex <= 0 -> baseQuery(chunkEntity.events, sender, unlinked).next(from = event.stateIndex)?.prevContent

View File

@ -18,8 +18,12 @@ package im.vector.matrix.android.internal.session.room.timeline


import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.timeline.Timeline 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.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.task.TaskExecutor
import im.vector.matrix.android.internal.util.fetchMappedCopied


internal class DefaultTimelineService(private val roomId: String, internal class DefaultTimelineService(private val roomId: String,
private val monarchy: Monarchy, 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) 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)
})
}

} }

View File

@ -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.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor
import io.realm.Realm


internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomMemberExtractor) { internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomMemberExtractor) {


private val cached = mutableMapOf<String, SenderData>() private val cached = mutableMapOf<String, SenderData>()


fun create(eventEntity: EventEntity): TimelineEvent { fun create(eventEntity: EventEntity, realm: Realm = eventEntity.realm): TimelineEvent {
val sender = eventEntity.sender val sender = eventEntity.sender
val cacheKey = sender + eventEntity.stateIndex val cacheKey = sender + eventEntity.stateIndex
val senderData = cached.getOrPut(cacheKey) { val senderData = cached.getOrPut(cacheKey) {
val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity) val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity,realm)
SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl) SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl)
} }
return TimelineEvent( return TimelineEvent(

View File

@ -42,6 +42,17 @@ fun <T : RealmModel> Monarchy.fetchCopied(query: (Realm) -> T?): T? {
return fetch(query, true) return fetch(query, true)
} }


fun <U, T : RealmModel> Monarchy.fetchMappedCopied(query: (Realm) -> T?, map: (T, realm: Realm) -> U): U? {
val ref = AtomicReference<U?>()
doWithRealm { realm ->
val result = query.invoke(realm)?.let {
map(realm.copyFromRealm(it), realm)
}
ref.set(result)
}
return ref.get()
}

private fun <T : RealmModel> Monarchy.fetch(query: (Realm) -> T?, copyFromRealm: Boolean): T? { private fun <T : RealmModel> Monarchy.fetch(query: (Realm) -> T?, copyFromRealm: Boolean): T? {
val ref = AtomicReference<T>() val ref = AtomicReference<T>()
doWithRealm { realm -> doWithRealm { realm ->

View File

@ -12,6 +12,7 @@ android {
targetSdkVersion 28 targetSdkVersion 28
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
multiDexEnabled true


testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 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-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$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.google.code.gson:gson:2.8.5'
implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'androidx.appcompat:appcompat:1.0.0-beta01' implementation 'androidx.appcompat:appcompat:1.0.0-beta01'

View File

@ -3,6 +3,10 @@
package="im.vector.reactions"> package="im.vector.reactions">


<application> <application>
<meta-data
android:name="fontProviderRequests"
android:value="Noto Color Emoji Compat" />

<activity <activity
android:name=".EmojiReactionPickerActivity" android:name=".EmojiReactionPickerActivity"
android:label="@string/title_activity_emoji_reaction_picker" android:label="@string/title_activity_emoji_reaction_picker"

View File

@ -41,7 +41,9 @@ class EmojiChooserFragment : Fragment() {


override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(EmojiChooserViewModel::class.java) viewModel = activity?.run {
ViewModelProviders.of(this).get(EmojiChooserViewModel::class.java)
} ?: throw Exception("Invalid Activity")
viewModel.initWithContect(context!!) viewModel.initWithContect(context!!)
(view as? RecyclerView)?.let { (view as? RecyclerView)?.let {
it.adapter = viewModel.adapter it.adapter = viewModel.adapter

View File

@ -16,14 +16,29 @@
package im.vector.reactions package im.vector.reactions


import android.content.Context import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel


class EmojiChooserViewModel : ViewModel() { class EmojiChooserViewModel : ViewModel() {


var adapter: EmojiRecyclerAdapter? = null var adapter: EmojiRecyclerAdapter? = null
val emojiSourceLiveData: MutableLiveData<EmojiDataSource> = MutableLiveData()

val currentSection: MutableLiveData<Int> = MutableLiveData()


fun initWithContect(context: Context) { 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)
}
} }

View File

@ -3,17 +3,14 @@ package im.vector.reactions
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface
import android.text.Layout
import android.text.StaticLayout import android.text.StaticLayout
import android.text.TextPaint import android.text.TextPaint
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.text.Layout import java.lang.Exception
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max




/** /**
@ -25,29 +22,26 @@ class EmojiDrawView @JvmOverloads constructor(
) : View(context, attrs, defStyleAttr) { ) : View(context, attrs, defStyleAttr) {


var mLayout: StaticLayout? = null var mLayout: StaticLayout? = null
set(value) {
field = value
invalidate()
}


// var _mySpacing = 0f // var _mySpacing = 0f


var emoji: String? = null var emoji: String? = null
set(value) { // set(value) {
field = value // if (value != null) {
if (value != null) { // EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.TextStaticLayout")
EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.TextStaticLayout") // mLayout = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
// GlobalScope.launch { // if (value != field) invalidate()
// val sl = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true) // EmojiRecyclerAdapter.endTraceSession()
// GlobalScope.launch(Dispatchers.Main) { // } else {
// if (emoji == value) { // mLayout = null
// mLayout = sl //// if (value != field) invalidate()
// //invalidate()
// } // }
// field = value
// } // }
// }
mLayout = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
EmojiRecyclerAdapter.endTraceSession()
} else {
mLayout = null
}
}


override fun onDraw(canvas: Canvas?) { override fun onDraw(canvas: Canvas?) {
EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.onDraw") EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.onDraw")
@ -55,7 +49,7 @@ class EmojiDrawView @JvmOverloads constructor(
canvas?.save() canvas?.save()
val space = abs((width - emojiSize) / 2f) val space = abs((width - emojiSize) / 2f)
if (mLayout == null) { if (mLayout == null) {
canvas?.drawCircle(width / 2f ,width / 2f, emojiSize / 2f,tPaint) // canvas?.drawCircle(width / 2f ,width / 2f, emojiSize / 2f,tPaint)
} else { } else {
canvas?.translate(space, space) canvas?.translate(space, space)
mLayout!!.draw(canvas) mLayout!!.draw(canvas)
@ -65,14 +59,18 @@ class EmojiDrawView @JvmOverloads constructor(
} }


companion object { 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.isAntiAlias = true;
tPaint.textSize = 24 * context.resources.displayMetrics.density tPaint.textSize = 24 * context.resources.displayMetrics.density
tPaint.color = Color.LTGRAY tPaint.color = Color.LTGRAY
typeface?.let {
tPaint.typeface = it
}

emojiSize = tPaint.measureText("😅").toInt() emojiSize = tPaint.measureText("😅").toInt()
} }
} }

View File

@ -15,34 +15,121 @@
*/ */
package im.vector.reactions package im.vector.reactions


import android.content.Context
import android.content.Intent
import android.graphics.Typeface
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.util.TypedValue
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.widget.SearchView import android.widget.SearchView
import androidx.appcompat.app.AppCompatActivity 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 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() { 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<TabLayout.Tab> {
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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

requestEmojivUnicode10CompatibleFont()


setContentView(R.layout.activity_emoji_reaction_picker) setContentView(R.layout.activity_emoji_reaction_picker)
setSupportActionBar(findViewById(R.id.toolbar)) setSupportActionBar(findViewById(R.id.toolbar))


tabLayout = findViewById(R.id.tabs) 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) 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 { override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater: MenuInflater = menuInflater val inflater: MenuInflater = menuInflater
inflater.inflate(R.menu.menu_emoji_reaction_picker, menu) inflater.inflate(R.menu.menu_emoji_reaction_picker, menu)
@ -54,20 +141,40 @@ class EmojiReactionPickerActivity : AppCompatActivity() {
override fun onMenuItemActionExpand(p0: MenuItem?): Boolean { override fun onMenuItemActionExpand(p0: MenuItem?): Boolean {
it.isIconified = false it.isIconified = false
it.requestFocusFromTouch() it.requestFocusFromTouch()
//we want to force the tool bar as visible even if hidden with scroll flags
findViewById<Toolbar>(R.id.toolbar)?.minimumHeight = getActionBarSize()
return true return true
} }


override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean { override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean {
// when back, clear all search // when back, clear all search
findViewById<Toolbar>(R.id.toolbar)?.minimumHeight = 0
it.setQuery("", true) it.setQuery("", true)
return true return 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
}
}
} }

View File

@ -15,66 +15,58 @@
*/ */
package im.vector.reactions package im.vector.reactions


import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import android.os.Trace import android.os.Trace
import android.text.Layout
import android.text.StaticLayout
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView 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) : class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() { RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() {


// data class EmojiInfo(val stringValue: String) var interactionListener: InteractionListener? = null
// data class SectionInfo(val sectionName: String) var mRecyclerView: RecyclerView? = null


//val mockData: ArrayList<Pair<SectionInfo, ArrayList<EmojiInfo>>> = ArrayList()


// val dataSource : EmojiDataSource? = null var currentFirstVisibleSection = 0

init {
// val faces = ArrayList<EmojiInfo>()
// for (i in 0..50) {
// faces.add(EmojiInfo("😅"))
// }
// val animalsNature = ArrayList<EmojiInfo>()
// for (i in 0..160) {
// animalsNature.add(EmojiInfo("🐶"))
// }
// val foods = ArrayList<EmojiInfo>()
// 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


enum class ScrollState {
IDLE,
DRAGGING,
SETTLING,
UNKNWON
} }


// enum class ScrollState { private var scrollState = ScrollState.UNKNWON
// IDLE, private var isFastScroll = false
// DRAGGING,
// SETTLING, val toUpdateWhenNotBusy = ArrayList<Pair<String, EmojiViewHolder>>()
// UNKNWON
// }


private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
}
}


override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView) super.onAttachedToRecyclerView(recyclerView)

this.mRecyclerView = recyclerView
EmojiDrawView.configureTextPaint(recyclerView.context)


val gridLayoutManager = GridLayoutManager(recyclerView.context, 8) val gridLayoutManager = GridLayoutManager(recyclerView.context, 8)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
@ -98,6 +90,24 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
recyclerView.addOnScrollListener(scrollListener) 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
beginTraceSession("MyAdapter.onCreateViewHolder") beginTraceSession("MyAdapter.onCreateViewHolder")
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
@ -162,7 +172,6 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
return sectionOffset return sectionOffset
} }


@SuppressLint("NewApi")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
beginTraceSession("MyAdapter.onBindViewHolder") beginTraceSession("MyAdapter.onBindViewHolder")
dataSource?.rawData?.categories?.let { categories -> dataSource?.rawData?.categories?.let { categories ->
@ -174,13 +183,36 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
val sectionOffset = getSectionOffset(sectionNumber) val sectionOffset = getSectionOffset(sectionNumber)
val emoji = sectionMojis[position - sectionOffset] val emoji = sectionMojis[position - sectionOffset]
val item = dataSource!!.rawData!!.emojis[emoji]!!.emojiString() val item = dataSource!!.rawData!!.emojis[emoji]!!.emojiString()
(holder as EmojiViewHolder).data = item
if (scrollState != ScrollState.SETTLING || !isFastScroll) {
// Log.i("PERF","Bind with draw at position:$position")
holder.bind(item) holder.bind(item)
} else {
// Log.i("PERF","Bind without draw at position:$position")
toUpdateWhenNotBusy.add(item to holder)
holder.bind(null)
}
} }
} }
endTraceSession() 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 { override fun getItemCount(): Int {
dataSource?.rawData?.categories?.let { dataSource?.rawData?.categories?.let {
var count = /*number of sections*/ it.size 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 class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(s: String) abstract fun bind(s: String?)
} }




class EmojiViewHolder(itemView: View) : ViewHolder(itemView) { class EmojiViewHolder(itemView: View) : ViewHolder(itemView) {


var emojiView: EmojiDrawView = itemView.findViewById(R.id.grid_item_emoji_text) 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 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) var textView: TextView = itemView.findViewById(R.id.section_header_textview)


override fun bind(s: String) { override fun bind(s: String?) {
textView.text = s textView.text = s
} }



} }


companion object { companion object {
@ -230,40 +272,75 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
Trace.beginSection(sectionName) Trace.beginSection(sectionName)
} }
} }

val staticLayoutCache = HashMap<String, StaticLayout>()

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<Pair<String, EmojiViewHolder>>
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<Int> = ArrayList()
// var sectionsInfo: ArrayList<Pair<Int, Int>> = 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
// }
// }
} }

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>

<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/pale_grey" />
</shape>

View File

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



View File

@ -5,6 +5,16 @@
android:layout_height="40dp" android:layout_height="40dp"
tools:showIn="@layout/activity_emoji_reaction_picker"> tools:showIn="@layout/activity_emoji_reaction_picker">


<View
android:id="@+id/grid_item_place_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_margin="4dp"
android:visibility="gone"
tools:visibility="visible"
android:background="@drawable/circle" />

<im.vector.reactions.EmojiDrawView <im.vector.reactions.EmojiDrawView
android:id="@+id/grid_item_emoji_text" android:id="@+id/grid_item_emoji_text"
android:layout_width="40dp" android:layout_width="40dp"

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="pale_grey">#f2f5f8</color>
<color name="light_blue_grey">#4ac1c9d6</color>
</resources>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="com_google_android_gms_fonts_certs">
<item>@array/com_google_android_gms_fonts_certs_dev</item>
<item>@array/com_google_android_gms_fonts_certs_prod</item>
</array>
<string-array name="com_google_android_gms_fonts_certs_dev">
<item>
MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
</item>
</string-array>
<string-array name="com_google_android_gms_fonts_certs_prod">
<item>
MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
</item>
</string-array>
</resources>

View File

@ -43,6 +43,16 @@
android:name=".core.services.CallService" android:name=".core.services.CallService"
android:exported="false" /> android:exported="false" />


<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/riotx_provider_paths" />
</provider>

</application> </application>


</manifest> </manifest>

View File

@ -18,6 +18,7 @@ package im.vector.riotredesign.core.platform


import com.airbnb.mvrx.BaseMvRxViewModel import com.airbnb.mvrx.BaseMvRxViewModel
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import im.vector.riotredesign.BuildConfig


abstract class VectorViewModel<S : MvRxState>(initialState: S) abstract class VectorViewModel<S : MvRxState>(initialState: S)
: BaseMvRxViewModel<S>(initialState, debugMode = false) : BaseMvRxViewModel<S>(initialState, debugMode = BuildConfig.DEBUG)

View File

@ -238,3 +238,26 @@ fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) {
activity.toast(R.string.error_no_external_application_found) 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)

}
}

View File

@ -76,10 +76,12 @@ fun requestDisablingBatteryOptimization(activity: Activity, fragment: Fragment?,
* @param context the context * @param context the context
* @param text the text to copy * @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 val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText("", text) clipboard.primaryClip = ClipData.newPlainText("", text)
if (showToast) {
context.toast(R.string.copied_to_clipboard) context.toast(R.string.copied_to_clipboard)
}
} }


/** /**

View File

@ -33,7 +33,7 @@ import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get import org.koin.android.ext.android.get


class EmptyState : MvRxState data class EmptyState(val isEmpty: Boolean = true) : MvRxState


class HomeActivityViewModel(state: EmptyState, class HomeActivityViewModel(state: EmptyState,
private val session: Session, private val session: Session,

View File

@ -20,16 +20,28 @@ import android.app.Activity.RESULT_OK
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Editable import android.text.Editable
import android.text.Spannable import android.text.Spannable
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog 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.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel 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.activity.FilePickerActivity
import com.jaiselrahman.filepicker.config.Configurations import com.jaiselrahman.filepicker.config.Configurations
import com.jaiselrahman.filepicker.model.MediaFile 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.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent 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.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.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.reactions.EmojiReactionPickerActivity
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.dialogs.DialogListItem import im.vector.riotredesign.core.dialogs.DialogListItem
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer 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.TextComposerViewModel
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState 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.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.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.html.PillImageSpan
import im.vector.riotredesign.features.media.ImageContentRenderer import im.vector.riotredesign.features.media.ImageContentRenderer
import im.vector.riotredesign.features.media.ImageMediaViewerActivity 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.android.scope.ext.android.getOrCreateScope
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import timber.log.Timber import timber.log.Timber
import java.io.File




@Parcelize @Parcelize
@ -88,7 +107,10 @@ private const val CAMERA_VALUE_TITLE = "attachment"
private const val REQUEST_FILES_REQUEST_CODE = 0 private const val REQUEST_FILES_REQUEST_CODE = 0
private const val TAKE_IMAGE_REQUEST_CODE = 1 private const val TAKE_IMAGE_REQUEST_CODE = 1


class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback { class RoomDetailFragment :
VectorBaseFragment(),
TimelineEventController.Callback,
AutocompleteUserPresenter.Callback {


companion object { companion object {


@ -115,8 +137,11 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac


override fun getLayoutResId() = R.layout.fragment_room_detail override fun getLayoutResId() = R.layout.fragment_room_detail


lateinit var actionViewModel: ActionsHandler

override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE)) bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
setupRecyclerView() setupRecyclerView()
setupToolbar() setupToolbar()
@ -125,6 +150,10 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
roomDetailViewModel.subscribe { renderState(it) } roomDetailViewModel.subscribe { renderState(it) }
textComposerViewModel.subscribe { renderTextComposerState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }

actionViewModel.actionCommandEvent.observe(this, Observer {
handleActions(it)
})
} }


override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@ -136,7 +165,6 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
} }
} }



override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
roomDetailViewModel.process(RoomDetailActions.IsDisplayed) roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
@ -402,8 +430,87 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
} }


// AutocompleteUserPresenter.Callback // 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?) { override fun onQueryUsers(query: CharSequence?) {
textComposerViewModel.process(TextComposerActions.QueryUsers(query)) textComposerViewModel.process(TextComposerActions.QueryUsers(query))
} }

private fun handleActions(it: LiveEvent<ActionsHandler.ActionData>?) {
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<TextView>(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()
}
}
}
}
} }

View File

@ -26,27 +26,17 @@ import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMember 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.*
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.timeline.Timeline 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.TimelineEvent
import im.vector.riotredesign.core.epoxy.LoadingItemModel_ import im.vector.riotredesign.core.epoxy.LoadingItemModel_
import im.vector.riotredesign.core.extensions.localDateTime 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.factory.TimelineItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper import im.vector.riotredesign.features.home.room.detail.timeline.helper.*
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.item.DaySeparatorItem 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.DaySeparatorItem_
import im.vector.riotredesign.features.home.room.detail.timeline.item.MergedHeaderItem 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.ImageContentRenderer
import im.vector.riotredesign.features.media.VideoContentRenderer import im.vector.riotredesign.features.media.VideoContentRenderer
import org.threeten.bp.LocalDateTime 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 onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
fun onFileMessageClicked(messageFileContent: MessageFileContent) fun onFileMessageClicked(messageFileContent: MessageFileContent)
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
fun onEventLongClicked(eventId: String, informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean
} }


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

View File

@ -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<LiveEvent<ActionData>>()

fun fireAction(actionId: String, data: Any? = null) {
actionCommandEvent.value = LiveEvent(ActionData(actionId,data))
}

}

View File

@ -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()
}
}

View File

@ -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<String>) {
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<View>(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 }

}
}
}

View File

@ -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<MessageActionState>(initialState) {

companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {

// override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
// //val currentSession = viewModelContext.activity.get<Session>()
// return MessageActionsViewModel(state/*,currentSession*/)
// }

override fun initialState(viewModelContext: ViewModelContext): MessageActionState? {
val currentSession = viewModelContext.activity.get<Session>()
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
}
}
}
}

View File

@ -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<ImageView>(R.id.action_icon)?.setImageResource(action.iconResId)
} else {
findViewById<ImageView>(R.id.action_icon)?.setImageDrawable(null)
}
findViewById<TextView>(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
}
}
}

View File

@ -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<SimpleAction>
) : MvRxState

class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<MessageMenuState>(initialState) {

companion object : MvRxViewModelFactory<MessageMenuViewModel, MessageMenuState> {

override fun initialState(viewModelContext: ViewModelContext): MessageMenuState? {
// Args are accessible from the context.
val currentSession = viewModelContext.activity.get<Session>()
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<SimpleAction>().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"


}
}

View File

@ -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<String>)
}

companion object {
fun newInstance(): QuickReactionFragment {
return QuickReactionFragment()
}
}
}

View File

@ -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<String>? = null) : MvRxState

class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel<QuickReactionState>(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<String> {
return ArrayList<String>(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<QuickReactionViewModel, QuickReactionState> {

override fun initialState(viewModelContext: ViewModelContext): QuickReactionState? {
// Args are accessible from the context.
// val foo = vieWModelContext.args<MyArgs>.foo
return QuickReactionState(TriggleState.NONE, TriggleState.NONE)
}
}
}

View File

@ -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.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.send.SendState
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.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
@ -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.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter 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.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem import im.vector.riotredesign.features.home.room.detail.timeline.item.*
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.html.EventHtmlRenderer import im.vector.riotredesign.features.html.EventHtmlRenderer
import im.vector.riotredesign.features.media.ImageContentRenderer import im.vector.riotredesign.features.media.ImageContentRenderer
import im.vector.riotredesign.features.media.VideoContentRenderer import im.vector.riotredesign.features.media.VideoContentRenderer
@ -93,11 +79,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
memberName = formattedMemberName, memberName = formattedMemberName,
showInformation = showInformation) showInformation = showInformation)


// val all = event.root.toContent()
// val ev = all.toModel<Event>()
return when (messageContent) { return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback) is MessageEmoteContent -> buildEmoteMessageItem(eventId, messageContent, informationData, callback)
is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback) is MessageTextContent -> buildTextMessageItem(eventId, event.sendState, messageContent, informationData, callback)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback) is MessageNoticeContent -> buildNoticeMessageItem(eventId, messageContent, informationData, callback)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback)
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback) is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback) is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback)
@ -179,7 +167,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
} }


private fun buildTextMessageItem(messageContent: MessageTextContent, private fun buildTextMessageItem(eventId: String, sendState: SendState,
messageContent: MessageTextContent,
informationData: MessageInformationData, informationData: MessageInformationData,
callback: TimelineEventController.Callback?): MessageTextItem? { callback: TimelineEventController.Callback?): MessageTextItem? {


@ -191,9 +180,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return MessageTextItem_() return MessageTextItem_()
.message(linkifiedBody) .message(linkifiedBody)
.informationData(informationData) .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, informationData: MessageInformationData,
callback: TimelineEventController.Callback?): MessageTextItem? { callback: TimelineEventController.Callback?): MessageTextItem? {


@ -208,9 +201,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return MessageTextItem_() return MessageTextItem_()
.message(message) .message(message)
.informationData(informationData) .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, informationData: MessageInformationData,
callback: TimelineEventController.Callback?): MessageTextItem? { callback: TimelineEventController.Callback?): MessageTextItem? {


@ -221,6 +218,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return MessageTextItem_() return MessageTextItem_()
.message(message) .message(message)
.informationData(informationData) .informationData(informationData)
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(eventId, informationData, messageContent, view)
?: false
}
} }


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

View File

@ -19,6 +19,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
@ -27,6 +28,9 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : VectorEpoxyModel<H>()


abstract val informationData: MessageInformationData abstract val informationData: MessageInformationData


@EpoxyAttribute
var longClickListener: View.OnLongClickListener? = null

override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
if (informationData.showInformation) { if (informationData.showInformation) {
@ -41,6 +45,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : VectorEpoxyModel<H>()
holder.memberNameView.visibility = View.GONE holder.memberNameView.visibility = View.GONE
holder.timeView.visibility = View.GONE holder.timeView.visibility = View.GONE
} }
holder.view.setOnLongClickListener(longClickListener)
} }


protected fun View.renderSendState() { protected fun View.renderSendState() {

View File

@ -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 im.vector.matrix.android.api.session.room.send.SendState


import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
data class MessageInformationData( data class MessageInformationData(
val eventId: String, val eventId: String,
val senderId: String, val senderId: String,
@ -26,4 +30,4 @@ data class MessageInformationData(
val avatarUrl: String?, val avatarUrl: String?,
val memberName: CharSequence? = null, val memberName: CharSequence? = null,
val showInformation: Boolean = true val showInformation: Boolean = true
) ) : Parcelable

View File

@ -46,6 +46,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
null) null)
holder.messageView.setTextFuture(textFuture) holder.messageView.setTextFuture(textFuture)
holder.messageView.renderSendState() holder.messageView.renderSendState()
holder.messageView.setOnLongClickListener(longClickListener)
findPillsAndProcess { it.bind(holder.messageView) } findPillsAndProcess { it.bind(holder.messageView) }
} }



View File

@ -16,6 +16,7 @@


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


import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
@ -33,9 +34,14 @@ abstract class NoticeItem : VectorEpoxyModel<NoticeItem.Holder>() {
@EpoxyAttribute var userId: String = "" @EpoxyAttribute var userId: String = ""
@EpoxyAttribute var memberName: CharSequence? = null @EpoxyAttribute var memberName: CharSequence? = null



@EpoxyAttribute
var longClickListener: View.OnLongClickListener? = null

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


class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View File

@ -160,6 +160,17 @@ object ThemeUtils {
return matchedColor 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 * Get the resource Id applied to the current theme
* *

View File

@ -0,0 +1,11 @@
<vector android:height="24dp" android:viewportHeight="22"
android:viewportWidth="22" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M10.032,8L18.968,8A2.032,2.032 0,0 1,21 10.032L21,18.968A2.032,2.032 0,0 1,18.968 21L10.032,21A2.032,2.032 0,0 1,8 18.968L8,10.032A2.032,2.032 0,0 1,10.032 8z"
android:strokeColor="#9E9E9E" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M4,14L3,14a2,2 0,0 1,-2 -2L1,3a2,2 0,0 1,2 -2h9a2,2 0,0 1,2 2v1"
android:strokeColor="#9E9E9E" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M14.75,8.5L21,14.75 14.75,21"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#9E9E9E"
android:strokeLineCap="round"/>
<path
android:pathData="M1,1v8.75a5,5 0,0 0,5 5h15"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#9E9E9E"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="21dp"
android:height="22dp"
android:viewportWidth="21"
android:viewportHeight="22">
<path
android:pathData="M9.497,3.06H2.888C1.845,3.06 1,3.93 1,5v13.576c0,1.07 0.845,1.94 1.888,1.94h13.218c1.042,0 1.888,-0.87 1.888,-1.94v-6.788"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#9E9E9E"
android:strokeLineCap="round"/>
<path
android:pathData="M16.578,1.606a1.966,1.966 0,0 1,2.832 0,2.097 2.097,0 0,1 0,2.91l-8.969,9.211 -3.776,0.97 0.944,-3.879 8.969,-9.212z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#9E9E9E"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="5dp"
android:viewportWidth="22"
android:viewportHeight="5">
<path
android:pathData="M9.333,2.5a1.429,1.5 0,1 0,2.858 0a1.429,1.5 0,1 0,-2.858 0z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#9E9E9E"
android:strokeLineCap="round"/>
<path
android:pathData="M17.666,2.5a1.429,1.5 0,1 0,2.858 0a1.429,1.5 0,1 0,-2.858 0z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#9E9E9E"
android:strokeLineCap="round"/>
<path
android:pathData="M1,2.5a1.429,1.5 0,1 0,2.858 0a1.429,1.5 0,1 0,-2.858 0z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#9E9E9E"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="5dp"
android:viewportWidth="22"
android:viewportHeight="5">
<path
android:pathData="M9.333,2.5a1.429,1.5 0,1 0,2.858 0a1.429,1.5 0,1 0,-2.858 0z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#9E9E9E"
android:strokeLineCap="round"/>
<path
android:pathData="M17.666,2.5a1.429,1.5 0,1 0,2.858 0a1.429,1.5 0,1 0,-2.858 0z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#9E9E9E"
android:strokeLineCap="round"/>
<path
android:pathData="M1,2.5a1.429,1.5 0,1 0,2.858 0a1.429,1.5 0,1 0,-2.858 0z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#9E9E9E"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="21dp"
android:height="21dp"
android:viewportWidth="21"
android:viewportHeight="21">
<path
android:pathData="M7.7782,11.7279L12.7279,6.7782A1,1 0,0 1,14.1421 6.7782L14.1421,6.7782A1,1 0,0 1,14.1421 8.1924L9.1924,13.1421A1,1 0,0 1,7.7782 13.1421L7.7782,13.1421A1,1 0,0 1,7.7782 11.7279z"
android:strokeWidth="1"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M6.9248,9.1065C6.7645,9.813 6.8139,10.5708 7.0693,11.2839C6.8715,11.3172 6.6817,11.4102 6.5291,11.5628L3.2857,14.8062C2.8952,15.1967 2.8952,15.8299 3.2857,16.2204L4.6999,17.6346C5.0904,18.0251 5.7236,18.0251 6.1141,17.6346L9.3575,14.3912C9.5102,14.2386 9.6031,14.0488 9.6364,13.851C10.3495,14.1064 11.1073,14.1558 11.8138,13.9955C11.7988,14.4866 11.6039,14.9733 11.229,15.3482L7.0711,19.5061C6.29,20.2871 5.0237,20.2871 4.2426,19.5061L1.4142,16.6777C0.6332,15.8966 0.6332,14.6303 1.4142,13.8492L5.5721,9.6913C5.947,9.3164 6.4337,9.1215 6.9248,9.1065Z"
android:strokeWidth="1"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M9.6269,6.4044C10.3334,6.2441 11.0913,6.2935 11.8043,6.5489C11.8376,6.351 11.9306,6.1613 12.0832,6.0086L15.3266,2.7653C15.7171,2.3748 16.3503,2.3748 16.7408,2.7653L18.155,4.1795C18.5455,4.57 18.5455,5.2032 18.155,5.5937L14.9117,8.8371C14.759,8.9897 14.5693,9.0827 14.3714,9.116C14.6268,9.829 14.6762,10.5869 14.5159,11.2934C15.0071,11.2784 15.4937,11.0834 15.8686,10.7086L20.0265,6.5506C20.8076,5.7696 20.8076,4.5033 20.0265,3.7222L17.1981,0.8938C16.417,0.1127 15.1507,0.1127 14.3697,0.8938L10.2117,5.0517C9.8369,5.4266 9.6419,5.9132 9.6269,6.4044Z"
android:strokeWidth="1"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M5.4861,9.7344L5.4501,9.3925L14.9912,4.5311L15.2479,4.7833C15.7127,5.24 16.3352,5.5 17,5.5C18.3807,5.5 19.5,4.3807 19.5,3C19.5,1.6193 18.3807,0.5 17,0.5C15.6193,0.5 14.5,1.6193 14.5,3C14.5,3.0963 14.5054,3.1918 14.5162,3.2863L14.5554,3.6308L5.0234,8.4876L4.7666,8.2311C4.3005,7.7656 3.6719,7.5 3,7.5C1.6193,7.5 0.5,8.6193 0.5,10C0.5,11.3807 1.6193,12.5 3,12.5C3.6072,12.5 4.1796,12.2834 4.6301,11.8955L4.8892,11.6724L14.493,16.7788L14.501,17.0699C14.5379,18.4208 15.6453,19.5 17,19.5C18.3807,19.5 19.5,18.3807 19.5,17C19.5,15.6193 18.3807,14.5 17,14.5C16.2197,14.5 15.4997,14.8592 15.0283,15.4628L14.77,15.7935L5.3947,10.8086L5.4598,10.4494C5.4865,10.3023 5.5,10.1521 5.5,10C5.5,9.9107 5.4953,9.8221 5.4861,9.7344Z"
android:strokeWidth="1"
android:fillColor="#979797"
android:strokeColor="#979797"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M11,11m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeLineCap="round"/>
<path
android:pathData="M7,13C7,13 8.5,15 11,15C13.5,15 15,13 15,13"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeLineCap="round"/>
<path
android:pathData="M7.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"
android:strokeWidth="1"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M14.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"
android:strokeWidth="1"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View File

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="17dp"
android:viewportWidth="22"
android:viewportHeight="17">
<path
android:pathData="M1.9413,8.3353L6.3938,12.3316C6.6985,12.6051 6.7238,13.0739 6.4503,13.3786C6.4325,13.3985 6.4136,13.4174 6.3938,13.4352C6.044,13.7491 5.514,13.7491 5.1643,13.4352L0.2462,9.021C0.0472,8.8423 -0.0327,8.5804 0.0121,8.3353C-0.0327,8.0902 0.0472,7.8283 0.2462,7.6496L5.1643,3.2354C5.514,2.9215 6.044,2.9215 6.3938,3.2354C6.4136,3.2532 6.4325,3.2721 6.4503,3.292C6.7238,3.5967 6.6985,4.0655 6.3938,4.339L1.9413,8.3353Z"
android:strokeWidth="1"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M19.6987,8.3353L15.2462,12.3316C14.9415,12.6051 14.9161,13.0739 15.1897,13.3786C15.2075,13.3985 15.2263,13.4174 15.2462,13.4352C15.5959,13.7491 16.126,13.7491 16.4757,13.4352L21.3938,9.021C21.5928,8.8423 21.6726,8.5804 21.6279,8.3353C21.6726,8.0902 21.5928,7.8283 21.3938,7.6496L16.4757,3.2354C16.126,2.9215 15.5959,2.9215 15.2462,3.2354C15.2263,3.2532 15.2075,3.2721 15.1897,3.292C14.9161,3.5967 14.9415,4.0655 15.2462,4.339L19.6987,8.3353Z"
android:strokeWidth="1"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M13,0.0467l2,0l-6.0855,16.9533l-2,0z"
android:strokeWidth="1"
android:fillColor="#9E9E9E"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View File

@ -6,10 +6,16 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:openDrawer="start"> tools:openDrawer="start">


<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">

<FrameLayout <FrameLayout
android:id="@+id/homeDetailFragmentContainer" android:id="@+id/homeDetailFragmentContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>




<FrameLayout <FrameLayout

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout_height="50dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="50dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingRight="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">

<ImageView
android:id="@+id/action_icon"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:src="@drawable/ic_material_delete"
android:tint="?android:attr/textColorSecondary" />

<TextView
android:id="@+id/action_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:textSize="17sp"
tools:text="@string/delete" />

</LinearLayout>

View File

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:id="@+id/root_layout"
android:layout_height="96dp">


<RelativeLayout
android:id="@+id/quick_react_1"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"

android:focusable="true"
app:layout_constraintBottom_toTopOf="@id/quick_react_agree_text"
app:layout_constraintEnd_toStartOf="@id/quick_react_2"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed">

<TextView
android:id="@+id/quick_react_1_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="30sp"
tools:text="👍" />
</RelativeLayout>

<RelativeLayout
android:id="@+id/quick_react_2"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="@id/quick_react_1"
app:layout_constraintEnd_toStartOf="@id/center_guideline"

app:layout_constraintStart_toEndOf="@id/quick_react_1"
app:layout_constraintTop_toTopOf="@id/quick_react_1">

<TextView
android:id="@+id/quick_react_2_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center_vertical"
android:textSize="30sp"
tools:text="👎" />
</RelativeLayout>

<TextView
android:id="@+id/quick_react_agree_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/reactions_agree"
android:textAlignment="center"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/center_guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/quick_react_1" />

<androidx.constraintlayout.widget.Guideline
android:id="@+id/center_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />

<RelativeLayout
android:id="@+id/quick_react_3"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="@+id/quick_react_1"
app:layout_constraintEnd_toStartOf="@id/quick_react_4"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/center_guideline"
app:layout_constraintTop_toTopOf="@id/quick_react_1">

<TextView
android:id="@+id/quick_react_3_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="30sp"
tools:text="😀" />
</RelativeLayout>

<RelativeLayout
android:id="@+id/quick_react_4"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="@id/quick_react_3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/quick_react_3"
app:layout_constraintTop_toTopOf="@id/quick_react_3">

<TextView
android:id="@+id/quick_react_4_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center_vertical"
android:textSize="30sp"
tools:text="😞" />
</RelativeLayout>

<TextView
android:id="@+id/quick_react_like_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/reactions_like"
android:textAlignment="center"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/quick_react_agree_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/center_guideline"
app:layout_constraintTop_toTopOf="@id/quick_react_agree_text" />

</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/bottom_sheet_behavior">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottom_sheet_message_preview"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<ImageView
android:id="@+id/bottom_sheet_message_preview_avatar"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="@dimen/layout_horizontal_margin"
android:adjustViewBounds="true"
android:contentDescription="@string/avatar"
android:background="@drawable/circle"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />

<TextView
android:id="@+id/bottom_sheet_message_preview_sender"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:ellipsize="end"
android:fontFamily="sans-serif-bold"
android:singleLine="true"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
tools:text="@tools:sample/full_names" />

<TextView xmlns:android="http://schemas.android.com/apk/res/android"

android:id="@+id/bottom_sheet_message_preview_body"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_gravity="start"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="@dimen/layout_vertical_margin"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?android:textColorSecondary"
android:textIsSelectable="false"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " />

</androidx.constraintlayout.widget.ConstraintLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color" />

<FrameLayout
android:id="@+id/bottom_sheet_quick_reaction_container"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color" />


<FrameLayout
android:id="@+id/bottom_sheet_menu_container"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<!--<com.airbnb.epoxy.EpoxyRecyclerView-->
<!--android:visibility="invisible"-->
<!--android:id="@+id/bottom_sheet_actions_list"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="match_parent"-->
<!--tools:itemCount="20"-->
<!--android:minHeight="80dp"-->
<!--tools:listitem="@layout/adapter_item_action">-->

<!--</com.airbnb.epoxy.EpoxyRecyclerView>-->
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/event_content_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:textSize="12sp"
android:fontFamily="monospace"
android:textIsSelectable="true" />
</ScrollView>

View File

@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingRight="16dp"> android:paddingRight="16dp">


@ -54,6 +55,7 @@
android:id="@+id/messageTextView" android:id="@+id/messageTextView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:layout_marginStart="64dp" android:layout_marginStart="64dp"
android:layout_marginLeft="64dp" android:layout_marginLeft="64dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"

View File

@ -57,6 +57,8 @@
<string name="stay">Stay</string> <string name="stay">Stay</string>
<string name="send">Send</string> <string name="send">Send</string>
<string name="copy">Copy</string> <string name="copy">Copy</string>
<string name="edit">Edit</string>
<string name="reply">Reply</string>
<string name="resend">Resend</string> <string name="resend">Resend</string>
<string name="redact">Remove</string> <string name="redact">Remove</string>
<string name="quote">Quote</string> <string name="quote">Quote</string>
@ -1400,4 +1402,9 @@ Why choose Riot.im?
<string name="autodiscover_well_known_autofill_dialog_message">Riot detected a custom server configuration for your userId domain \"%s\":\n%s</string> <string name="autodiscover_well_known_autofill_dialog_message">Riot detected a custom server configuration for your userId domain \"%s\":\n%s</string>
<string name="autodiscover_well_known_autofill_confirm">Use Config</string> <string name="autodiscover_well_known_autofill_confirm">Use Config</string>
<string name="reactions_agree">Agree</string>
<string name="reactions_like">Like</string>
<string name="message_add_reaction">Add Reaction</string>

</resources> </resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path
name="shared"
path="/" />
</paths>