[WIP] Emoji Reactions
This commit is contained in:
parent
a64f509872
commit
56a2a3a065
@ -8,7 +8,8 @@ buildscript {
|
||||
jcenter()
|
||||
maven {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
} }
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
|
@ -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
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model.message
|
||||
|
||||
|
||||
interface MessageContent {
|
||||
val type: String
|
||||
val body: String
|
||||
|
@ -18,9 +18,13 @@ package im.vector.matrix.android.api.session.room.send
|
||||
|
||||
enum class SendState {
|
||||
UNKNOWN,
|
||||
// the event has not been sent
|
||||
UNSENT,
|
||||
// the event is encrypting
|
||||
ENCRYPTING,
|
||||
// the event is currently sending
|
||||
SENDING,
|
||||
// the event has been sent
|
||||
SENT,
|
||||
SYNCED;
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
package im.vector.matrix.android.api.session.room.timeline
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
|
||||
@ -59,4 +60,8 @@ data class TimelineEvent(
|
||||
inline fun <reified T> getMetadata(key: String): T? {
|
||||
return metadata[key] as T?
|
||||
}
|
||||
|
||||
fun isEncrypted() : Boolean {
|
||||
return EventType.ENCRYPTED == root.type
|
||||
}
|
||||
}
|
||||
|
@ -30,4 +30,6 @@ interface TimelineService {
|
||||
*/
|
||||
fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline
|
||||
|
||||
|
||||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||
}
|
@ -28,17 +28,18 @@ import im.vector.matrix.android.internal.database.query.findIncludingEvent
|
||||
import im.vector.matrix.android.internal.database.query.next
|
||||
import im.vector.matrix.android.internal.database.query.prev
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmQuery
|
||||
|
||||
internal class SenderRoomMemberExtractor(private val roomId: String) {
|
||||
|
||||
fun extractFrom(event: EventEntity): RoomMember? {
|
||||
fun extractFrom(event: EventEntity, realm: Realm = event.realm): RoomMember? {
|
||||
val sender = event.sender ?: return null
|
||||
// If the event is unlinked we want to fetch unlinked state events
|
||||
val unlinked = event.isUnlinked
|
||||
val roomEntity = RoomEntity.where(event.realm, roomId = roomId).findFirst() ?: return null
|
||||
val chunkEntity = ChunkEntity.findIncludingEvent(event.realm, event.eventId)
|
||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return null
|
||||
val chunkEntity = ChunkEntity.findIncludingEvent(realm, event.eventId)
|
||||
val content = when {
|
||||
chunkEntity == null -> null
|
||||
event.stateIndex <= 0 -> baseQuery(chunkEntity.events, sender, unlinked).next(from = event.stateIndex)?.prevContent
|
||||
|
@ -18,8 +18,12 @@ package im.vector.matrix.android.internal.session.room.timeline
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.util.fetchMappedCopied
|
||||
|
||||
internal class DefaultTimelineService(private val roomId: String,
|
||||
private val monarchy: Monarchy,
|
||||
@ -33,4 +37,12 @@ internal class DefaultTimelineService(private val roomId: String,
|
||||
return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, allowedTypes)
|
||||
}
|
||||
|
||||
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
|
||||
return monarchy.fetchMappedCopied({
|
||||
EventEntity.where(it, eventId = eventId).findFirst()
|
||||
}, { entity, realm ->
|
||||
timelineEventFactory.create(entity, realm)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
@ -20,16 +20,17 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor
|
||||
import io.realm.Realm
|
||||
|
||||
internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomMemberExtractor) {
|
||||
|
||||
private val cached = mutableMapOf<String, SenderData>()
|
||||
|
||||
fun create(eventEntity: EventEntity): TimelineEvent {
|
||||
fun create(eventEntity: EventEntity, realm: Realm = eventEntity.realm): TimelineEvent {
|
||||
val sender = eventEntity.sender
|
||||
val cacheKey = sender + eventEntity.stateIndex
|
||||
val senderData = cached.getOrPut(cacheKey) {
|
||||
val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity)
|
||||
val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity,realm)
|
||||
SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl)
|
||||
}
|
||||
return TimelineEvent(
|
||||
|
@ -42,6 +42,17 @@ fun <T : RealmModel> Monarchy.fetchCopied(query: (Realm) -> T?): T? {
|
||||
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? {
|
||||
val ref = AtomicReference<T>()
|
||||
doWithRealm { realm ->
|
||||
|
@ -12,6 +12,7 @@ android {
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
multiDexEnabled true
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@ -36,6 +37,9 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
// Log
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.8.5'
|
||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.0-beta01'
|
||||
|
@ -3,6 +3,10 @@
|
||||
package="im.vector.reactions">
|
||||
|
||||
<application>
|
||||
<meta-data
|
||||
android:name="fontProviderRequests"
|
||||
android:value="Noto Color Emoji Compat" />
|
||||
|
||||
<activity
|
||||
android:name=".EmojiReactionPickerActivity"
|
||||
android:label="@string/title_activity_emoji_reaction_picker"
|
||||
|
@ -41,7 +41,9 @@ class EmojiChooserFragment : Fragment() {
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
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!!)
|
||||
(view as? RecyclerView)?.let {
|
||||
it.adapter = viewModel.adapter
|
||||
|
@ -16,14 +16,29 @@
|
||||
package im.vector.reactions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class EmojiChooserViewModel : ViewModel() {
|
||||
|
||||
var adapter: EmojiRecyclerAdapter? = null
|
||||
val emojiSourceLiveData: MutableLiveData<EmojiDataSource> = MutableLiveData()
|
||||
|
||||
val currentSection: MutableLiveData<Int> = MutableLiveData()
|
||||
|
||||
fun initWithContect(context: Context) {
|
||||
adapter = EmojiRecyclerAdapter(EmojiDataSource(context))
|
||||
val emojiDataSource = EmojiDataSource(context)
|
||||
emojiSourceLiveData.value = emojiDataSource
|
||||
adapter = EmojiRecyclerAdapter(emojiDataSource)
|
||||
adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener {
|
||||
override fun firstVisibleSectionChange(section: Int) {
|
||||
currentSection.value = section
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun scrollToSection(sectionIndex: Int) {
|
||||
adapter?.scrollToSection(sectionIndex)
|
||||
}
|
||||
}
|
||||
|
@ -3,17 +3,14 @@ package im.vector.reactions
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.text.Layout
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.Exception
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
/**
|
||||
@ -25,29 +22,26 @@ class EmojiDrawView @JvmOverloads constructor(
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
var mLayout: StaticLayout? = null
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
// var _mySpacing = 0f
|
||||
|
||||
var emoji: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (value != null) {
|
||||
EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.TextStaticLayout")
|
||||
// GlobalScope.launch {
|
||||
// val sl = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
|
||||
// GlobalScope.launch(Dispatchers.Main) {
|
||||
// if (emoji == value) {
|
||||
// mLayout = sl
|
||||
// //invalidate()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
mLayout = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
|
||||
EmojiRecyclerAdapter.endTraceSession()
|
||||
} else {
|
||||
mLayout = null
|
||||
}
|
||||
}
|
||||
// set(value) {
|
||||
// if (value != null) {
|
||||
// EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.TextStaticLayout")
|
||||
// mLayout = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
|
||||
// if (value != field) invalidate()
|
||||
// EmojiRecyclerAdapter.endTraceSession()
|
||||
// } else {
|
||||
// mLayout = null
|
||||
//// if (value != field) invalidate()
|
||||
// }
|
||||
// field = value
|
||||
// }
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.onDraw")
|
||||
@ -55,7 +49,7 @@ class EmojiDrawView @JvmOverloads constructor(
|
||||
canvas?.save()
|
||||
val space = abs((width - emojiSize) / 2f)
|
||||
if (mLayout == null) {
|
||||
canvas?.drawCircle(width / 2f ,width / 2f, emojiSize / 2f,tPaint)
|
||||
// canvas?.drawCircle(width / 2f ,width / 2f, emojiSize / 2f,tPaint)
|
||||
} else {
|
||||
canvas?.translate(space, space)
|
||||
mLayout!!.draw(canvas)
|
||||
@ -65,14 +59,18 @@ class EmojiDrawView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val tPaint = TextPaint()
|
||||
val tPaint = TextPaint()
|
||||
|
||||
private var emojiSize = 40
|
||||
var emojiSize = 40
|
||||
|
||||
fun configureTextPaint(context: Context) {
|
||||
fun configureTextPaint(context: Context, typeface: Typeface?) {
|
||||
tPaint.isAntiAlias = true;
|
||||
tPaint.textSize = 24 * context.resources.displayMetrics.density
|
||||
tPaint.color = Color.LTGRAY
|
||||
typeface?.let {
|
||||
tPaint.typeface = it
|
||||
}
|
||||
|
||||
emojiSize = tPaint.measureText("😅").toInt()
|
||||
}
|
||||
}
|
||||
|
@ -15,34 +15,121 @@
|
||||
*/
|
||||
package im.vector.reactions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.util.TypedValue
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.widget.SearchView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.provider.FontRequest
|
||||
import androidx.core.provider.FontsContractCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* TODO: Loading indicator while getting emoji data source?
|
||||
* TODO: migrate to maverick
|
||||
*/
|
||||
class EmojiReactionPickerActivity : AppCompatActivity() {
|
||||
|
||||
lateinit var tabLayout: TabLayout
|
||||
private lateinit var tabLayout: TabLayout
|
||||
|
||||
lateinit var viewModel: EmojiChooserViewModel
|
||||
|
||||
private var mHandler: Handler? = null
|
||||
|
||||
private var tabLayoutSelectionListener = object : TabLayout.BaseOnTabSelectedListener<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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
requestEmojivUnicode10CompatibleFont()
|
||||
|
||||
|
||||
setContentView(R.layout.activity_emoji_reaction_picker)
|
||||
setSupportActionBar(findViewById(R.id.toolbar))
|
||||
|
||||
tabLayout = findViewById(R.id.tabs)
|
||||
|
||||
|
||||
tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
|
||||
tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
|
||||
tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
|
||||
|
||||
viewModel = ViewModelProviders.of(this).get(EmojiChooserViewModel::class.java)
|
||||
|
||||
viewModel.emojiSourceLiveData.observe(this, Observer {
|
||||
it.rawData?.categories?.let { categories ->
|
||||
for (category in categories) {
|
||||
val s = category.emojis[0]
|
||||
tabLayout.addTab(tabLayout.newTab().setText(it.rawData!!.emojis[s]!!.emojiString()))
|
||||
}
|
||||
tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
viewModel.currentSection.observe(this, Observer { section ->
|
||||
section?.let {
|
||||
tabLayout.removeOnTabSelectedListener(tabLayoutSelectionListener)
|
||||
tabLayout.getTabAt(it)?.select()
|
||||
tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener)
|
||||
}
|
||||
})
|
||||
supportActionBar?.title = getString(R.string.title_activity_emoji_reaction_picker)
|
||||
}
|
||||
|
||||
private fun requestEmojivUnicode10CompatibleFont() {
|
||||
val fontRequest = FontRequest(
|
||||
"com.google.android.gms.fonts",
|
||||
"com.google.android.gms",
|
||||
"Noto Color Emoji Compat",
|
||||
R.array.com_google_android_gms_fonts_certs
|
||||
)
|
||||
|
||||
EmojiDrawView.configureTextPaint(this, null)
|
||||
val callback = object : FontsContractCompat.FontRequestCallback() {
|
||||
|
||||
override fun onTypefaceRetrieved(typeface: Typeface) {
|
||||
EmojiDrawView.configureTextPaint(this@EmojiReactionPickerActivity, typeface)
|
||||
}
|
||||
|
||||
override fun onTypefaceRequestFailed(reason: Int) {
|
||||
Timber.e("Failed to load Emoji Compatible font, reason:$reason")
|
||||
}
|
||||
}
|
||||
|
||||
FontsContractCompat.requestFont(this, fontRequest, callback, getFontThreadHandler())
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val inflater: MenuInflater = menuInflater
|
||||
inflater.inflate(R.menu.menu_emoji_reaction_picker, menu)
|
||||
@ -54,20 +141,40 @@ class EmojiReactionPickerActivity : AppCompatActivity() {
|
||||
override fun onMenuItemActionExpand(p0: MenuItem?): Boolean {
|
||||
it.isIconified = false
|
||||
it.requestFocusFromTouch()
|
||||
//we want to force the tool bar as visible even if hidden with scroll flags
|
||||
findViewById<Toolbar>(R.id.toolbar)?.minimumHeight = getActionBarSize()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean {
|
||||
// when back, clear all search
|
||||
findViewById<Toolbar>(R.id.toolbar)?.minimumHeight = 0
|
||||
it.setQuery("", true)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
//TODO move to ThemeUtils when core module is created
|
||||
private fun getActionBarSize(): Int {
|
||||
return try {
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(R.attr.actionBarSize, typedValue, true)
|
||||
TypedValue.complexToDimensionPixelSize(typedValue.data, resources.displayMetrics)
|
||||
} catch (e: Exception) {
|
||||
//Timber.e(e, "Unable to get color")
|
||||
TypedValue.complexToDimensionPixelSize(56, resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun intent(context: Context): Intent {
|
||||
val intent = Intent(context, EmojiReactionPickerActivity::class.java)
|
||||
// intent.putExtra(EXTRA_MATRIX_ID, matrixID)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,66 +15,58 @@
|
||||
*/
|
||||
package im.vector.reactions
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Trace
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.TransitionManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* TODO: Configure Span using available width and emoji size
|
||||
* TODO: Search
|
||||
* TODO: Performances
|
||||
* TODO: Scroll to section - Find a way to snap section to the top
|
||||
*/
|
||||
class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
||||
RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() {
|
||||
|
||||
// data class EmojiInfo(val stringValue: String)
|
||||
// data class SectionInfo(val sectionName: String)
|
||||
var interactionListener: InteractionListener? = null
|
||||
var mRecyclerView: RecyclerView? = null
|
||||
|
||||
//val mockData: ArrayList<Pair<SectionInfo, ArrayList<EmojiInfo>>> = ArrayList()
|
||||
|
||||
// val dataSource : EmojiDataSource? = null
|
||||
|
||||
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
|
||||
var currentFirstVisibleSection = 0
|
||||
|
||||
enum class ScrollState {
|
||||
IDLE,
|
||||
DRAGGING,
|
||||
SETTLING,
|
||||
UNKNWON
|
||||
}
|
||||
|
||||
// enum class ScrollState {
|
||||
// IDLE,
|
||||
// DRAGGING,
|
||||
// SETTLING,
|
||||
// UNKNWON
|
||||
// }
|
||||
private var scrollState = ScrollState.UNKNWON
|
||||
private var isFastScroll = false
|
||||
|
||||
val toUpdateWhenNotBusy = ArrayList<Pair<String, EmojiViewHolder>>()
|
||||
|
||||
private val scrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
|
||||
EmojiDrawView.configureTextPaint(recyclerView.context)
|
||||
this.mRecyclerView = recyclerView
|
||||
|
||||
val gridLayoutManager = GridLayoutManager(recyclerView.context, 8)
|
||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
@ -98,6 +90,24 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
||||
recyclerView.addOnScrollListener(scrollListener)
|
||||
}
|
||||
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
this.mRecyclerView = null
|
||||
recyclerView.removeOnScrollListener(scrollListener)
|
||||
staticLayoutCache.clear()
|
||||
super.onDetachedFromRecyclerView(recyclerView)
|
||||
}
|
||||
|
||||
fun scrollToSection(section: Int) {
|
||||
if (section < 0 || section >= dataSource?.rawData?.categories?.size ?: 0) {
|
||||
//ignore
|
||||
return
|
||||
}
|
||||
//mRecyclerView?.smoothScrollToPosition(getSectionOffset(section) - 1)
|
||||
//TODO Snap section header to top
|
||||
mRecyclerView?.scrollToPosition(getSectionOffset(section) - 1)
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
beginTraceSession("MyAdapter.onCreateViewHolder")
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
@ -162,7 +172,6 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
||||
return sectionOffset
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
beginTraceSession("MyAdapter.onBindViewHolder")
|
||||
dataSource?.rawData?.categories?.let { categories ->
|
||||
@ -174,13 +183,36 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
||||
val sectionOffset = getSectionOffset(sectionNumber)
|
||||
val emoji = sectionMojis[position - sectionOffset]
|
||||
val item = dataSource!!.rawData!!.emojis[emoji]!!.emojiString()
|
||||
holder.bind(item)
|
||||
(holder as EmojiViewHolder).data = item
|
||||
if (scrollState != ScrollState.SETTLING || !isFastScroll) {
|
||||
// Log.i("PERF","Bind with draw at position:$position")
|
||||
holder.bind(item)
|
||||
} else {
|
||||
// Log.i("PERF","Bind without draw at position:$position")
|
||||
toUpdateWhenNotBusy.add(item to holder)
|
||||
holder.bind(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
endTraceSession()
|
||||
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
if (holder is EmojiViewHolder) {
|
||||
holder.data = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
toUpdateWhenNotBusy.removeIf { it.second == holder }
|
||||
} else {
|
||||
val index = toUpdateWhenNotBusy.indexOfFirst { it.second == holder }
|
||||
if (index != -1) {
|
||||
toUpdateWhenNotBusy.removeAt(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onViewRecycled(holder)
|
||||
}
|
||||
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
dataSource?.rawData?.categories?.let {
|
||||
var count = /*number of sections*/ it.size
|
||||
@ -193,17 +225,28 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
||||
|
||||
|
||||
abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
abstract fun bind(s: String)
|
||||
abstract fun bind(s: String?)
|
||||
}
|
||||
|
||||
|
||||
class EmojiViewHolder(itemView: View) : ViewHolder(itemView) {
|
||||
|
||||
var emojiView: EmojiDrawView = itemView.findViewById(R.id.grid_item_emoji_text)
|
||||
val placeHolder: View = itemView.findViewById(R.id.grid_item_place_holder)
|
||||
|
||||
var data: String? = null
|
||||
|
||||
override fun bind(s: String) {
|
||||
override fun bind(s: String?) {
|
||||
emojiView.emoji = s
|
||||
if (s != null) {
|
||||
emojiView.mLayout = getStaticLayoutForEmoji(s)
|
||||
placeHolder.visibility = View.GONE
|
||||
// emojiView.visibility = View.VISIBLE
|
||||
} else {
|
||||
emojiView.mLayout = null
|
||||
placeHolder.visibility = View.VISIBLE
|
||||
// emojiView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,11 +254,10 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
||||
|
||||
var textView: TextView = itemView.findViewById(R.id.section_header_textview)
|
||||
|
||||
override fun bind(s: String) {
|
||||
override fun bind(s: String?) {
|
||||
textView.text = s
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -230,40 +272,75 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
||||
Trace.beginSection(sectionName)
|
||||
}
|
||||
}
|
||||
|
||||
val staticLayoutCache = HashMap<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
|
||||
// }
|
||||
// }
|
||||
}
|
6
reactions/src/main/res/drawable/circle.xml
Normal file
6
reactions/src/main/res/drawable/circle.xml
Normal 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>
|
@ -57,7 +57,7 @@
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="30dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp" />
|
||||
|
||||
|
@ -5,6 +5,16 @@
|
||||
android:layout_height="40dp"
|
||||
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
|
||||
android:id="@+id/grid_item_emoji_text"
|
||||
android:layout_width="40dp"
|
||||
|
5
reactions/src/main/res/values/colors.xml
Normal file
5
reactions/src/main/res/values/colors.xml
Normal 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>
|
17
reactions/src/main/res/values/font_certs.xml
Normal file
17
reactions/src/main/res/values/font_certs.xml
Normal 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>
|
@ -43,6 +43,16 @@
|
||||
android:name=".core.services.CallService"
|
||||
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>
|
||||
|
||||
</manifest>
|
@ -18,6 +18,7 @@ package im.vector.riotredesign.core.platform
|
||||
|
||||
import com.airbnb.mvrx.BaseMvRxViewModel
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import im.vector.riotredesign.BuildConfig
|
||||
|
||||
abstract class VectorViewModel<S : MvRxState>(initialState: S)
|
||||
: BaseMvRxViewModel<S>(initialState, debugMode = false)
|
||||
: BaseMvRxViewModel<S>(initialState, debugMode = BuildConfig.DEBUG)
|
@ -238,3 +238,26 @@ fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) {
|
||||
activity.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
|
||||
fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
|
||||
|
||||
var mediaUri: Uri? = null
|
||||
try {
|
||||
mediaUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", file)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("onMediaAction Selected File cannot be shared " + e.message)
|
||||
}
|
||||
|
||||
|
||||
if (null != mediaUri) {
|
||||
val sendIntent = Intent()
|
||||
sendIntent.action = Intent.ACTION_SEND
|
||||
// Grant temporary read permission to the content URI
|
||||
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
sendIntent.type = mediaMimeType
|
||||
sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri)
|
||||
|
||||
context.startActivity(sendIntent)
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -76,10 +76,12 @@ fun requestDisablingBatteryOptimization(activity: Activity, fragment: Fragment?,
|
||||
* @param context the context
|
||||
* @param text the text to copy
|
||||
*/
|
||||
fun copyToClipboard(context: Context, text: CharSequence) {
|
||||
fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = true) {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.primaryClip = ClipData.newPlainText("", text)
|
||||
context.toast(R.string.copied_to_clipboard)
|
||||
if (showToast) {
|
||||
context.toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,7 +33,7 @@ import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import org.koin.android.ext.android.get
|
||||
|
||||
class EmptyState : MvRxState
|
||||
data class EmptyState(val isEmpty: Boolean = true) : MvRxState
|
||||
|
||||
class HomeActivityViewModel(state: EmptyState,
|
||||
private val session: Session,
|
||||
|
@ -20,16 +20,28 @@ import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.text.Editable
|
||||
import android.text.Spannable
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.github.piasy.biv.BigImageViewer
|
||||
import com.github.piasy.biv.loader.ImageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.jaiselrahman.filepicker.activity.FilePickerActivity
|
||||
import com.jaiselrahman.filepicker.config.Configurations
|
||||
import com.jaiselrahman.filepicker.model.MediaFile
|
||||
@ -41,8 +53,10 @@ import im.vector.matrix.android.api.session.room.model.message.MessageAudioConte
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.dialogs.DialogListItem
|
||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||
@ -62,7 +76,11 @@ import im.vector.riotredesign.features.home.room.detail.composer.TextComposerAct
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
|
||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.action.ActionsHandler
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageMenuViewModel
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotredesign.features.html.PillImageSpan
|
||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||
import im.vector.riotredesign.features.media.ImageMediaViewerActivity
|
||||
@ -75,6 +93,7 @@ import org.koin.android.scope.ext.android.bindScope
|
||||
import org.koin.android.scope.ext.android.getOrCreateScope
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
|
||||
@Parcelize
|
||||
@ -88,7 +107,10 @@ private const val CAMERA_VALUE_TITLE = "attachment"
|
||||
private const val REQUEST_FILES_REQUEST_CODE = 0
|
||||
private const val TAKE_IMAGE_REQUEST_CODE = 1
|
||||
|
||||
class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback {
|
||||
class RoomDetailFragment :
|
||||
VectorBaseFragment(),
|
||||
TimelineEventController.Callback,
|
||||
AutocompleteUserPresenter.Callback {
|
||||
|
||||
companion object {
|
||||
|
||||
@ -115,8 +137,11 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_room_detail
|
||||
|
||||
lateinit var actionViewModel: ActionsHandler
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
||||
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
|
||||
setupRecyclerView()
|
||||
setupToolbar()
|
||||
@ -125,6 +150,10 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
||||
roomDetailViewModel.subscribe { renderState(it) }
|
||||
textComposerViewModel.subscribe { renderTextComposerState(it) }
|
||||
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
|
||||
|
||||
actionViewModel.actionCommandEvent.observe(this, Observer {
|
||||
handleActions(it)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
@ -136,7 +165,6 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
|
||||
@ -402,8 +430,87 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
||||
}
|
||||
|
||||
// AutocompleteUserPresenter.Callback
|
||||
override fun onEventLongClicked(eventId: String, informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
val roomId = (arguments?.get(MvRx.KEY_ARG) as? RoomDetailArgs)?.roomId
|
||||
if (roomId.isNullOrBlank()) {
|
||||
Timber.e("Missing RoomId, cannot open bottomsheet")
|
||||
return false
|
||||
}
|
||||
MessageActionsBottomSheet
|
||||
.newInstance(eventId, roomId, informationData)
|
||||
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// AutocompleteUserPresenter.Callback
|
||||
|
||||
override fun onQueryUsers(query: CharSequence?) {
|
||||
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
||||
}
|
||||
|
||||
private fun handleActions(it: LiveEvent<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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,27 +26,17 @@ import com.airbnb.epoxy.EpoxyController
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.core.epoxy.LoadingItemModel_
|
||||
import im.vector.riotredesign.core.extensions.localDateTime
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineAsyncHelper
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.canBeMerged
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.nextDisplayableEvent
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.prevSameTypeEvents
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.*
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MergedHeaderItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||
import org.threeten.bp.LocalDateTime
|
||||
@ -64,6 +54,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
|
||||
fun onFileMessageClicked(messageFileContent: MessageFileContent)
|
||||
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
|
||||
fun onEventLongClicked(eventId: String, informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean
|
||||
}
|
||||
|
||||
private val collapsedEventIds = linkedSetOf<String>()
|
||||
@ -170,8 +161,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
||||
// We then are sure we always have items up to date.
|
||||
if (modelCache[position] == null
|
||||
|| modelCache[position]?.mergedHeaderModel != null
|
||||
|| modelCache[position]?.formattedDayModel != null) {
|
||||
|| modelCache[position]?.mergedHeaderModel != null
|
||||
|| modelCache[position]?.formattedDayModel != null) {
|
||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||
}
|
||||
}
|
||||
@ -245,7 +236,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
// => handle case where paginating from mergeable events and we get more
|
||||
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
|
||||
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
|
||||
?: true
|
||||
?: true
|
||||
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
|
||||
if (isCollapsed) {
|
||||
collapsedEventIds.addAll(mergedEventIds)
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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 }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -23,14 +23,8 @@ import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
@ -40,15 +34,7 @@ import im.vector.riotredesign.core.resources.ColorProvider
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem_
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageFileItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageFileItem_
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem_
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem_
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.*
|
||||
import im.vector.riotredesign.features.html.EventHtmlRenderer
|
||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||
@ -70,13 +56,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
||||
?: false
|
||||
?: false
|
||||
|
||||
val showInformation = addDaySeparator
|
||||
|| event.senderAvatar != nextEvent?.senderAvatar
|
||||
|| event.senderName != nextEvent?.senderName
|
||||
|| nextEvent?.root?.type != EventType.MESSAGE
|
||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||
|| event.senderAvatar != nextEvent?.senderAvatar
|
||||
|| event.senderName != nextEvent?.senderName
|
||||
|| nextEvent?.root?.type != EventType.MESSAGE
|
||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||
|
||||
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
||||
val time = timelineDateFormatter.formatMessageHour(date)
|
||||
@ -86,22 +72,24 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
textColor = colorProvider.getColor(getColorFor(event.root.sender ?: ""))
|
||||
}
|
||||
val informationData = MessageInformationData(eventId = eventId,
|
||||
senderId = event.root.sender ?: "",
|
||||
sendState = event.sendState,
|
||||
time = time,
|
||||
avatarUrl = avatarUrl,
|
||||
memberName = formattedMemberName,
|
||||
showInformation = showInformation)
|
||||
senderId = event.root.sender ?: "",
|
||||
sendState = event.sendState,
|
||||
time = time,
|
||||
avatarUrl = avatarUrl,
|
||||
memberName = formattedMemberName,
|
||||
showInformation = showInformation)
|
||||
|
||||
// val all = event.root.toContent()
|
||||
// val ev = all.toModel<Event>()
|
||||
return when (messageContent) {
|
||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback)
|
||||
is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback)
|
||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
|
||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback)
|
||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback)
|
||||
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback)
|
||||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback)
|
||||
else -> buildNotHandledMessageItem(messageContent)
|
||||
is MessageEmoteContent -> buildEmoteMessageItem(eventId, messageContent, informationData, callback)
|
||||
is MessageTextContent -> buildTextMessageItem(eventId, event.sendState, messageContent, informationData, callback)
|
||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
|
||||
is MessageNoticeContent -> buildNoticeMessageItem(eventId, messageContent, informationData, callback)
|
||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback)
|
||||
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback)
|
||||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback)
|
||||
else -> buildNotHandledMessageItem(messageContent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +167,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
|
||||
}
|
||||
|
||||
private fun buildTextMessageItem(messageContent: MessageTextContent,
|
||||
private fun buildTextMessageItem(eventId: String, sendState: SendState,
|
||||
messageContent: MessageTextContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
@ -191,9 +180,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
return MessageTextItem_()
|
||||
.message(linkifiedBody)
|
||||
.informationData(informationData)
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(eventId, informationData, messageContent, view)
|
||||
?: false
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent,
|
||||
private fun buildNoticeMessageItem(eventId: String, messageContent: MessageNoticeContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
@ -208,9 +201,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
return MessageTextItem_()
|
||||
.message(message)
|
||||
.informationData(informationData)
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(eventId, informationData, messageContent, view)
|
||||
?: false
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
|
||||
private fun buildEmoteMessageItem(eventId: String, messageContent: MessageEmoteContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
@ -221,6 +218,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
return MessageTextItem_()
|
||||
.message(message)
|
||||
.informationData(informationData)
|
||||
.longClickListener { view ->
|
||||
return@longClickListener callback?.onEventLongClicked(eventId, informationData, messageContent, view)
|
||||
?: false
|
||||
}
|
||||
}
|
||||
|
||||
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): Spannable {
|
||||
@ -251,13 +252,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
}
|
||||
val cI = Math.abs(hash) % 8 + 1
|
||||
return when (cI) {
|
||||
1 -> R.color.username_1
|
||||
2 -> R.color.username_2
|
||||
3 -> R.color.username_3
|
||||
4 -> R.color.username_4
|
||||
5 -> R.color.username_5
|
||||
6 -> R.color.username_6
|
||||
7 -> R.color.username_7
|
||||
1 -> R.color.username_1
|
||||
2 -> R.color.username_2
|
||||
3 -> R.color.username_3
|
||||
4 -> R.color.username_4
|
||||
5 -> R.color.username_5
|
||||
6 -> R.color.username_6
|
||||
7 -> R.color.username_7
|
||||
else -> R.color.username_8
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
@ -27,6 +28,9 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : VectorEpoxyModel<H>()
|
||||
|
||||
abstract val informationData: MessageInformationData
|
||||
|
||||
@EpoxyAttribute
|
||||
var longClickListener: View.OnLongClickListener? = null
|
||||
|
||||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
if (informationData.showInformation) {
|
||||
@ -41,6 +45,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : VectorEpoxyModel<H>()
|
||||
holder.memberNameView.visibility = View.GONE
|
||||
holder.timeView.visibility = View.GONE
|
||||
}
|
||||
holder.view.setOnLongClickListener(longClickListener)
|
||||
}
|
||||
|
||||
protected fun View.renderSendState() {
|
||||
|
@ -18,6 +18,10 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class MessageInformationData(
|
||||
val eventId: String,
|
||||
val senderId: String,
|
||||
@ -26,4 +30,4 @@ data class MessageInformationData(
|
||||
val avatarUrl: String?,
|
||||
val memberName: CharSequence? = null,
|
||||
val showInformation: Boolean = true
|
||||
)
|
||||
) : Parcelable
|
@ -46,6 +46,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
null)
|
||||
holder.messageView.setTextFuture(textFuture)
|
||||
holder.messageView.renderSendState()
|
||||
holder.messageView.setOnLongClickListener(longClickListener)
|
||||
findPillsAndProcess { it.bind(holder.messageView) }
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
@ -33,9 +34,14 @@ abstract class NoticeItem : VectorEpoxyModel<NoticeItem.Holder>() {
|
||||
@EpoxyAttribute var userId: String = ""
|
||||
@EpoxyAttribute var memberName: CharSequence? = null
|
||||
|
||||
|
||||
@EpoxyAttribute
|
||||
var longClickListener: View.OnLongClickListener? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.noticeTextView.text = noticeText
|
||||
AvatarRenderer.render(avatarUrl, userId, memberName?.toString(), holder.avatarImageView)
|
||||
holder.view.setOnLongClickListener(longClickListener)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
|
@ -160,6 +160,17 @@ object ThemeUtils {
|
||||
return matchedColor
|
||||
}
|
||||
|
||||
fun getAttribute(c: Context, @AttrRes attribute: Int): TypedValue? {
|
||||
try {
|
||||
val typedValue = TypedValue()
|
||||
c.theme.resolveAttribute(attribute, typedValue, true)
|
||||
return typedValue
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Unable to get color")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource Id applied to the current theme
|
||||
*
|
||||
|
11
vector/src/main/res/drawable/ic_copy.xml
Normal file
11
vector/src/main/res/drawable/ic_copy.xml
Normal 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>
|
22
vector/src/main/res/drawable/ic_corner_down_right.xml
Normal file
22
vector/src/main/res/drawable/ic_corner_down_right.xml
Normal 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>
|
22
vector/src/main/res/drawable/ic_edit.xml
Normal file
22
vector/src/main/res/drawable/ic_edit.xml
Normal 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>
|
30
vector/src/main/res/drawable/ic_more_horizontal.xml
Normal file
30
vector/src/main/res/drawable/ic_more_horizontal.xml
Normal 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>
|
30
vector/src/main/res/drawable/ic_more_horizontal_2.xml
Normal file
30
vector/src/main/res/drawable/ic_more_horizontal_2.xml
Normal 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>
|
24
vector/src/main/res/drawable/ic_permalink.xml
Normal file
24
vector/src/main/res/drawable/ic_permalink.xml
Normal 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>
|
12
vector/src/main/res/drawable/ic_share.xml
Normal file
12
vector/src/main/res/drawable/ic_share.xml
Normal 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>
|
34
vector/src/main/res/drawable/ic_smile.xml
Normal file
34
vector/src/main/res/drawable/ic_smile.xml
Normal 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>
|
24
vector/src/main/res/drawable/ic_view_source.xml
Normal file
24
vector/src/main/res/drawable/ic_view_source.xml
Normal 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>
|
@ -6,10 +6,16 @@
|
||||
android:layout_height="match_parent"
|
||||
tools:openDrawer="start">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/homeDetailFragmentContainer"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/coordinatorLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/homeDetailFragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
|
||||
<FrameLayout
|
||||
|
37
vector/src/main/res/layout/adapter_item_action.xml
Normal file
37
vector/src/main/res/layout/adapter_item_action.xml
Normal 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>
|
@ -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>
|
108
vector/src/main/res/layout/bottom_sheet_message_actions.xml
Normal file
108
vector/src/main/res/layout/bottom_sheet_message_actions.xml
Normal 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>
|
15
vector/src/main/res/layout/dialog_event_content.xml
Normal file
15
vector/src/main/res/layout/dialog_event_content.xml
Normal 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>
|
@ -4,6 +4,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
|
||||
@ -54,6 +55,7 @@
|
||||
android:id="@+id/messageTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:foreground="?attr/selectableItemBackgroundBorderless"
|
||||
android:layout_marginStart="64dp"
|
||||
android:layout_marginLeft="64dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
|
@ -57,6 +57,8 @@
|
||||
<string name="stay">Stay</string>
|
||||
<string name="send">Send</string>
|
||||
<string name="copy">Copy</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="reply">Reply</string>
|
||||
<string name="resend">Resend</string>
|
||||
<string name="redact">Remove</string>
|
||||
<string name="quote">Quote</string>
|
||||
@ -1399,5 +1401,10 @@ Why choose Riot.im?
|
||||
<string name="autodiscover_well_known_autofill_dialog_title">"Autocomplete Server Options</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="reactions_agree">Agree</string>
|
||||
<string name="reactions_like">Like</string>
|
||||
<string name="message_add_reaction">Add Reaction</string>
|
||||
|
||||
</resources>
|
||||
|
6
vector/src/main/res/xml/riotx_provider_paths.xml
Normal file
6
vector/src/main/res/xml/riotx_provider_paths.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path
|
||||
name="shared"
|
||||
path="/" />
|
||||
</paths>
|
Loading…
x
Reference in New Issue
Block a user