forked from GitHub-Mirror/riotX-android
[WIP] Emoji Reactions
This commit is contained in:
parent
a64f509872
commit
56a2a3a065
@ -8,7 +8,8 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
maven {
|
maven {
|
||||||
url "https://plugins.gradle.org/m2/"
|
url "https://plugins.gradle.org/m2/"
|
||||||
} }
|
}
|
||||||
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||||
classpath 'com.google.gms:google-services:4.2.0'
|
classpath 'com.google.gms:google-services:4.2.0'
|
||||||
|
@ -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
|
package im.vector.matrix.android.api.session.room.model.message
|
||||||
|
|
||||||
|
|
||||||
interface MessageContent {
|
interface MessageContent {
|
||||||
val type: String
|
val type: String
|
||||||
val body: String
|
val body: String
|
||||||
|
@ -18,9 +18,13 @@ package im.vector.matrix.android.api.session.room.send
|
|||||||
|
|
||||||
enum class SendState {
|
enum class SendState {
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
|
// the event has not been sent
|
||||||
UNSENT,
|
UNSENT,
|
||||||
|
// the event is encrypting
|
||||||
ENCRYPTING,
|
ENCRYPTING,
|
||||||
|
// the event is currently sending
|
||||||
SENDING,
|
SENDING,
|
||||||
|
// the event has been sent
|
||||||
SENT,
|
SENT,
|
||||||
SYNCED;
|
SYNCED;
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package im.vector.matrix.android.api.session.room.timeline
|
package im.vector.matrix.android.api.session.room.timeline
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
|
|
||||||
@ -59,4 +60,8 @@ data class TimelineEvent(
|
|||||||
inline fun <reified T> getMetadata(key: String): T? {
|
inline fun <reified T> getMetadata(key: String): T? {
|
||||||
return metadata[key] as T?
|
return metadata[key] as T?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isEncrypted() : Boolean {
|
||||||
|
return EventType.ENCRYPTED == root.type
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,4 +30,6 @@ interface TimelineService {
|
|||||||
*/
|
*/
|
||||||
fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline
|
fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline
|
||||||
|
|
||||||
|
|
||||||
|
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||||
}
|
}
|
@ -28,17 +28,18 @@ import im.vector.matrix.android.internal.database.query.findIncludingEvent
|
|||||||
import im.vector.matrix.android.internal.database.query.next
|
import im.vector.matrix.android.internal.database.query.next
|
||||||
import im.vector.matrix.android.internal.database.query.prev
|
import im.vector.matrix.android.internal.database.query.prev
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import io.realm.Realm
|
||||||
import io.realm.RealmList
|
import io.realm.RealmList
|
||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
|
|
||||||
internal class SenderRoomMemberExtractor(private val roomId: String) {
|
internal class SenderRoomMemberExtractor(private val roomId: String) {
|
||||||
|
|
||||||
fun extractFrom(event: EventEntity): RoomMember? {
|
fun extractFrom(event: EventEntity, realm: Realm = event.realm): RoomMember? {
|
||||||
val sender = event.sender ?: return null
|
val sender = event.sender ?: return null
|
||||||
// If the event is unlinked we want to fetch unlinked state events
|
// If the event is unlinked we want to fetch unlinked state events
|
||||||
val unlinked = event.isUnlinked
|
val unlinked = event.isUnlinked
|
||||||
val roomEntity = RoomEntity.where(event.realm, roomId = roomId).findFirst() ?: return null
|
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return null
|
||||||
val chunkEntity = ChunkEntity.findIncludingEvent(event.realm, event.eventId)
|
val chunkEntity = ChunkEntity.findIncludingEvent(realm, event.eventId)
|
||||||
val content = when {
|
val content = when {
|
||||||
chunkEntity == null -> null
|
chunkEntity == null -> null
|
||||||
event.stateIndex <= 0 -> baseQuery(chunkEntity.events, sender, unlinked).next(from = event.stateIndex)?.prevContent
|
event.stateIndex <= 0 -> baseQuery(chunkEntity.events, sender, unlinked).next(from = event.stateIndex)?.prevContent
|
||||||
|
@ -18,8 +18,12 @@ package im.vector.matrix.android.internal.session.room.timeline
|
|||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import im.vector.matrix.android.internal.util.fetchMappedCopied
|
||||||
|
|
||||||
internal class DefaultTimelineService(private val roomId: String,
|
internal class DefaultTimelineService(private val roomId: String,
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
@ -33,4 +37,12 @@ internal class DefaultTimelineService(private val roomId: String,
|
|||||||
return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, allowedTypes)
|
return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, allowedTypes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
|
||||||
|
return monarchy.fetchMappedCopied({
|
||||||
|
EventEntity.where(it, eventId = eventId).findFirst()
|
||||||
|
}, { entity, realm ->
|
||||||
|
timelineEventFactory.create(entity, realm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -20,16 +20,17 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
|||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor
|
import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor
|
||||||
|
import io.realm.Realm
|
||||||
|
|
||||||
internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomMemberExtractor) {
|
internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomMemberExtractor) {
|
||||||
|
|
||||||
private val cached = mutableMapOf<String, SenderData>()
|
private val cached = mutableMapOf<String, SenderData>()
|
||||||
|
|
||||||
fun create(eventEntity: EventEntity): TimelineEvent {
|
fun create(eventEntity: EventEntity, realm: Realm = eventEntity.realm): TimelineEvent {
|
||||||
val sender = eventEntity.sender
|
val sender = eventEntity.sender
|
||||||
val cacheKey = sender + eventEntity.stateIndex
|
val cacheKey = sender + eventEntity.stateIndex
|
||||||
val senderData = cached.getOrPut(cacheKey) {
|
val senderData = cached.getOrPut(cacheKey) {
|
||||||
val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity)
|
val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity,realm)
|
||||||
SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl)
|
SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl)
|
||||||
}
|
}
|
||||||
return TimelineEvent(
|
return TimelineEvent(
|
||||||
|
@ -42,6 +42,17 @@ fun <T : RealmModel> Monarchy.fetchCopied(query: (Realm) -> T?): T? {
|
|||||||
return fetch(query, true)
|
return fetch(query, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <U, T : RealmModel> Monarchy.fetchMappedCopied(query: (Realm) -> T?, map: (T, realm: Realm) -> U): U? {
|
||||||
|
val ref = AtomicReference<U?>()
|
||||||
|
doWithRealm { realm ->
|
||||||
|
val result = query.invoke(realm)?.let {
|
||||||
|
map(realm.copyFromRealm(it), realm)
|
||||||
|
}
|
||||||
|
ref.set(result)
|
||||||
|
}
|
||||||
|
return ref.get()
|
||||||
|
}
|
||||||
|
|
||||||
private fun <T : RealmModel> Monarchy.fetch(query: (Realm) -> T?, copyFromRealm: Boolean): T? {
|
private fun <T : RealmModel> Monarchy.fetch(query: (Realm) -> T?, copyFromRealm: Boolean): T? {
|
||||||
val ref = AtomicReference<T>()
|
val ref = AtomicReference<T>()
|
||||||
doWithRealm { realm ->
|
doWithRealm { realm ->
|
||||||
|
@ -12,6 +12,7 @@ android {
|
|||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
|
multiDexEnabled true
|
||||||
|
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@ -36,6 +37,9 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
|
|
||||||
|
// Log
|
||||||
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
|
||||||
implementation 'com.google.code.gson:gson:2.8.5'
|
implementation 'com.google.code.gson:gson:2.8.5'
|
||||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.0.0-beta01'
|
implementation 'androidx.appcompat:appcompat:1.0.0-beta01'
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
package="im.vector.reactions">
|
package="im.vector.reactions">
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
|
<meta-data
|
||||||
|
android:name="fontProviderRequests"
|
||||||
|
android:value="Noto Color Emoji Compat" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".EmojiReactionPickerActivity"
|
android:name=".EmojiReactionPickerActivity"
|
||||||
android:label="@string/title_activity_emoji_reaction_picker"
|
android:label="@string/title_activity_emoji_reaction_picker"
|
||||||
|
@ -41,7 +41,9 @@ class EmojiChooserFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
viewModel = ViewModelProviders.of(this).get(EmojiChooserViewModel::class.java)
|
viewModel = activity?.run {
|
||||||
|
ViewModelProviders.of(this).get(EmojiChooserViewModel::class.java)
|
||||||
|
} ?: throw Exception("Invalid Activity")
|
||||||
viewModel.initWithContect(context!!)
|
viewModel.initWithContect(context!!)
|
||||||
(view as? RecyclerView)?.let {
|
(view as? RecyclerView)?.let {
|
||||||
it.adapter = viewModel.adapter
|
it.adapter = viewModel.adapter
|
||||||
|
@ -16,14 +16,29 @@
|
|||||||
package im.vector.reactions
|
package im.vector.reactions
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
class EmojiChooserViewModel : ViewModel() {
|
class EmojiChooserViewModel : ViewModel() {
|
||||||
|
|
||||||
var adapter: EmojiRecyclerAdapter? = null
|
var adapter: EmojiRecyclerAdapter? = null
|
||||||
|
val emojiSourceLiveData: MutableLiveData<EmojiDataSource> = MutableLiveData()
|
||||||
|
|
||||||
|
val currentSection: MutableLiveData<Int> = MutableLiveData()
|
||||||
|
|
||||||
fun initWithContect(context: Context) {
|
fun initWithContect(context: Context) {
|
||||||
adapter = EmojiRecyclerAdapter(EmojiDataSource(context))
|
val emojiDataSource = EmojiDataSource(context)
|
||||||
|
emojiSourceLiveData.value = emojiDataSource
|
||||||
|
adapter = EmojiRecyclerAdapter(emojiDataSource)
|
||||||
|
adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener {
|
||||||
|
override fun firstVisibleSectionChange(section: Int) {
|
||||||
|
currentSection.value = section
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scrollToSection(sectionIndex: Int) {
|
||||||
|
adapter?.scrollToSection(sectionIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,17 +3,14 @@ package im.vector.reactions
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.Layout
|
||||||
import android.text.StaticLayout
|
import android.text.StaticLayout
|
||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.text.Layout
|
import java.lang.Exception
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,29 +22,26 @@ class EmojiDrawView @JvmOverloads constructor(
|
|||||||
) : View(context, attrs, defStyleAttr) {
|
) : View(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
var mLayout: StaticLayout? = null
|
var mLayout: StaticLayout? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
// var _mySpacing = 0f
|
// var _mySpacing = 0f
|
||||||
|
|
||||||
var emoji: String? = null
|
var emoji: String? = null
|
||||||
set(value) {
|
// set(value) {
|
||||||
field = value
|
// if (value != null) {
|
||||||
if (value != null) {
|
// EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.TextStaticLayout")
|
||||||
EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.TextStaticLayout")
|
// mLayout = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
|
||||||
// GlobalScope.launch {
|
// if (value != field) invalidate()
|
||||||
// val sl = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
|
// EmojiRecyclerAdapter.endTraceSession()
|
||||||
// GlobalScope.launch(Dispatchers.Main) {
|
// } else {
|
||||||
// if (emoji == value) {
|
// mLayout = null
|
||||||
// mLayout = sl
|
//// if (value != field) invalidate()
|
||||||
// //invalidate()
|
|
||||||
// }
|
// }
|
||||||
|
// field = value
|
||||||
// }
|
// }
|
||||||
// }
|
|
||||||
mLayout = StaticLayout(value, tPaint, emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
|
|
||||||
EmojiRecyclerAdapter.endTraceSession()
|
|
||||||
} else {
|
|
||||||
mLayout = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas?) {
|
override fun onDraw(canvas: Canvas?) {
|
||||||
EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.onDraw")
|
EmojiRecyclerAdapter.beginTraceSession("EmojiDrawView.onDraw")
|
||||||
@ -55,7 +49,7 @@ class EmojiDrawView @JvmOverloads constructor(
|
|||||||
canvas?.save()
|
canvas?.save()
|
||||||
val space = abs((width - emojiSize) / 2f)
|
val space = abs((width - emojiSize) / 2f)
|
||||||
if (mLayout == null) {
|
if (mLayout == null) {
|
||||||
canvas?.drawCircle(width / 2f ,width / 2f, emojiSize / 2f,tPaint)
|
// canvas?.drawCircle(width / 2f ,width / 2f, emojiSize / 2f,tPaint)
|
||||||
} else {
|
} else {
|
||||||
canvas?.translate(space, space)
|
canvas?.translate(space, space)
|
||||||
mLayout!!.draw(canvas)
|
mLayout!!.draw(canvas)
|
||||||
@ -65,14 +59,18 @@ class EmojiDrawView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val tPaint = TextPaint()
|
val tPaint = TextPaint()
|
||||||
|
|
||||||
private var emojiSize = 40
|
var emojiSize = 40
|
||||||
|
|
||||||
fun configureTextPaint(context: Context) {
|
fun configureTextPaint(context: Context, typeface: Typeface?) {
|
||||||
tPaint.isAntiAlias = true;
|
tPaint.isAntiAlias = true;
|
||||||
tPaint.textSize = 24 * context.resources.displayMetrics.density
|
tPaint.textSize = 24 * context.resources.displayMetrics.density
|
||||||
tPaint.color = Color.LTGRAY
|
tPaint.color = Color.LTGRAY
|
||||||
|
typeface?.let {
|
||||||
|
tPaint.typeface = it
|
||||||
|
}
|
||||||
|
|
||||||
emojiSize = tPaint.measureText("😅").toInt()
|
emojiSize = tPaint.measureText("😅").toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,34 +15,121 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.reactions
|
package im.vector.reactions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.HandlerThread
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.SearchView
|
import android.widget.SearchView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.provider.FontRequest
|
||||||
|
import androidx.core.provider.FontsContractCompat
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* TODO: Loading indicator while getting emoji data source?
|
||||||
|
* TODO: migrate to maverick
|
||||||
|
*/
|
||||||
class EmojiReactionPickerActivity : AppCompatActivity() {
|
class EmojiReactionPickerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
lateinit var tabLayout: TabLayout
|
private lateinit var tabLayout: TabLayout
|
||||||
|
|
||||||
|
lateinit var viewModel: EmojiChooserViewModel
|
||||||
|
|
||||||
|
private var mHandler: Handler? = null
|
||||||
|
|
||||||
|
private var tabLayoutSelectionListener = object : TabLayout.BaseOnTabSelectedListener<TabLayout.Tab> {
|
||||||
|
override fun onTabReselected(p0: TabLayout.Tab) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTabUnselected(p0: TabLayout.Tab) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTabSelected(p0: TabLayout.Tab) {
|
||||||
|
viewModel.scrollToSection(p0.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFontThreadHandler(): Handler {
|
||||||
|
if (mHandler == null) {
|
||||||
|
val handlerThread = HandlerThread("fonts")
|
||||||
|
handlerThread.start()
|
||||||
|
mHandler = Handler(handlerThread.looper)
|
||||||
|
}
|
||||||
|
return mHandler!!
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
requestEmojivUnicode10CompatibleFont()
|
||||||
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_emoji_reaction_picker)
|
setContentView(R.layout.activity_emoji_reaction_picker)
|
||||||
setSupportActionBar(findViewById(R.id.toolbar))
|
setSupportActionBar(findViewById(R.id.toolbar))
|
||||||
|
|
||||||
tabLayout = findViewById(R.id.tabs)
|
tabLayout = findViewById(R.id.tabs)
|
||||||
|
|
||||||
|
|
||||||
tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
|
|
||||||
tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
|
|
||||||
tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
|
|
||||||
|
|
||||||
|
viewModel = ViewModelProviders.of(this).get(EmojiChooserViewModel::class.java)
|
||||||
|
|
||||||
|
viewModel.emojiSourceLiveData.observe(this, Observer {
|
||||||
|
it.rawData?.categories?.let { categories ->
|
||||||
|
for (category in categories) {
|
||||||
|
val s = category.emojis[0]
|
||||||
|
tabLayout.addTab(tabLayout.newTab().setText(it.rawData!!.emojis[s]!!.emojiString()))
|
||||||
|
}
|
||||||
|
tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
viewModel.currentSection.observe(this, Observer { section ->
|
||||||
|
section?.let {
|
||||||
|
tabLayout.removeOnTabSelectedListener(tabLayoutSelectionListener)
|
||||||
|
tabLayout.getTabAt(it)?.select()
|
||||||
|
tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener)
|
||||||
|
}
|
||||||
|
})
|
||||||
supportActionBar?.title = getString(R.string.title_activity_emoji_reaction_picker)
|
supportActionBar?.title = getString(R.string.title_activity_emoji_reaction_picker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun requestEmojivUnicode10CompatibleFont() {
|
||||||
|
val fontRequest = FontRequest(
|
||||||
|
"com.google.android.gms.fonts",
|
||||||
|
"com.google.android.gms",
|
||||||
|
"Noto Color Emoji Compat",
|
||||||
|
R.array.com_google_android_gms_fonts_certs
|
||||||
|
)
|
||||||
|
|
||||||
|
EmojiDrawView.configureTextPaint(this, null)
|
||||||
|
val callback = object : FontsContractCompat.FontRequestCallback() {
|
||||||
|
|
||||||
|
override fun onTypefaceRetrieved(typeface: Typeface) {
|
||||||
|
EmojiDrawView.configureTextPaint(this@EmojiReactionPickerActivity, typeface)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTypefaceRequestFailed(reason: Int) {
|
||||||
|
Timber.e("Failed to load Emoji Compatible font, reason:$reason")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FontsContractCompat.requestFont(this, fontRequest, callback, getFontThreadHandler())
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
val inflater: MenuInflater = menuInflater
|
val inflater: MenuInflater = menuInflater
|
||||||
inflater.inflate(R.menu.menu_emoji_reaction_picker, menu)
|
inflater.inflate(R.menu.menu_emoji_reaction_picker, menu)
|
||||||
@ -54,20 +141,40 @@ class EmojiReactionPickerActivity : AppCompatActivity() {
|
|||||||
override fun onMenuItemActionExpand(p0: MenuItem?): Boolean {
|
override fun onMenuItemActionExpand(p0: MenuItem?): Boolean {
|
||||||
it.isIconified = false
|
it.isIconified = false
|
||||||
it.requestFocusFromTouch()
|
it.requestFocusFromTouch()
|
||||||
|
//we want to force the tool bar as visible even if hidden with scroll flags
|
||||||
|
findViewById<Toolbar>(R.id.toolbar)?.minimumHeight = getActionBarSize()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean {
|
override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean {
|
||||||
// when back, clear all search
|
// when back, clear all search
|
||||||
|
findViewById<Toolbar>(R.id.toolbar)?.minimumHeight = 0
|
||||||
it.setQuery("", true)
|
it.setQuery("", true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO move to ThemeUtils when core module is created
|
||||||
|
private fun getActionBarSize(): Int {
|
||||||
|
return try {
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
theme.resolveAttribute(R.attr.actionBarSize, typedValue, true)
|
||||||
|
TypedValue.complexToDimensionPixelSize(typedValue.data, resources.displayMetrics)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
//Timber.e(e, "Unable to get color")
|
||||||
|
TypedValue.complexToDimensionPixelSize(56, resources.displayMetrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun intent(context: Context): Intent {
|
||||||
|
val intent = Intent(context, EmojiReactionPickerActivity::class.java)
|
||||||
|
// intent.putExtra(EXTRA_MATRIX_ID, matrixID)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,66 +15,58 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.reactions
|
package im.vector.reactions
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Trace
|
import android.os.Trace
|
||||||
|
import android.text.Layout
|
||||||
|
import android.text.StaticLayout
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.transition.AutoTransition
|
||||||
|
import androidx.transition.TransitionManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* TODO: Configure Span using available width and emoji size
|
||||||
|
* TODO: Search
|
||||||
|
* TODO: Performances
|
||||||
|
* TODO: Scroll to section - Find a way to snap section to the top
|
||||||
|
*/
|
||||||
class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
||||||
RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() {
|
RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() {
|
||||||
|
|
||||||
// data class EmojiInfo(val stringValue: String)
|
var interactionListener: InteractionListener? = null
|
||||||
// data class SectionInfo(val sectionName: String)
|
var mRecyclerView: RecyclerView? = null
|
||||||
|
|
||||||
//val mockData: ArrayList<Pair<SectionInfo, ArrayList<EmojiInfo>>> = ArrayList()
|
|
||||||
|
|
||||||
// val dataSource : EmojiDataSource? = null
|
var currentFirstVisibleSection = 0
|
||||||
|
|
||||||
init {
|
|
||||||
// val faces = ArrayList<EmojiInfo>()
|
|
||||||
// for (i in 0..50) {
|
|
||||||
// faces.add(EmojiInfo("😅"))
|
|
||||||
// }
|
|
||||||
// val animalsNature = ArrayList<EmojiInfo>()
|
|
||||||
// for (i in 0..160) {
|
|
||||||
// animalsNature.add(EmojiInfo("🐶"))
|
|
||||||
// }
|
|
||||||
// val foods = ArrayList<EmojiInfo>()
|
|
||||||
// for (i in 0..150) {
|
|
||||||
// foods.add(EmojiInfo("🍎"))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// mockData.add(SectionInfo("Smiley & People") to faces)
|
|
||||||
// mockData.add(SectionInfo("Animals & Nature") to animalsNature)
|
|
||||||
// mockData.add(SectionInfo("Food & Drinks") to foods)
|
|
||||||
// dataSource = EMp
|
|
||||||
|
|
||||||
|
enum class ScrollState {
|
||||||
|
IDLE,
|
||||||
|
DRAGGING,
|
||||||
|
SETTLING,
|
||||||
|
UNKNWON
|
||||||
}
|
}
|
||||||
|
|
||||||
// enum class ScrollState {
|
private var scrollState = ScrollState.UNKNWON
|
||||||
// IDLE,
|
private var isFastScroll = false
|
||||||
// DRAGGING,
|
|
||||||
// SETTLING,
|
val toUpdateWhenNotBusy = ArrayList<Pair<String, EmojiViewHolder>>()
|
||||||
// UNKNWON
|
|
||||||
// }
|
|
||||||
|
|
||||||
private val scrollListener = object : RecyclerView.OnScrollListener() {
|
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
||||||
super.onScrollStateChanged(recyclerView, newState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||||
super.onAttachedToRecyclerView(recyclerView)
|
super.onAttachedToRecyclerView(recyclerView)
|
||||||
|
this.mRecyclerView = recyclerView
|
||||||
EmojiDrawView.configureTextPaint(recyclerView.context)
|
|
||||||
|
|
||||||
val gridLayoutManager = GridLayoutManager(recyclerView.context, 8)
|
val gridLayoutManager = GridLayoutManager(recyclerView.context, 8)
|
||||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||||
@ -98,6 +90,24 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
|||||||
recyclerView.addOnScrollListener(scrollListener)
|
recyclerView.addOnScrollListener(scrollListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||||
|
this.mRecyclerView = null
|
||||||
|
recyclerView.removeOnScrollListener(scrollListener)
|
||||||
|
staticLayoutCache.clear()
|
||||||
|
super.onDetachedFromRecyclerView(recyclerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scrollToSection(section: Int) {
|
||||||
|
if (section < 0 || section >= dataSource?.rawData?.categories?.size ?: 0) {
|
||||||
|
//ignore
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//mRecyclerView?.smoothScrollToPosition(getSectionOffset(section) - 1)
|
||||||
|
//TODO Snap section header to top
|
||||||
|
mRecyclerView?.scrollToPosition(getSectionOffset(section) - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
beginTraceSession("MyAdapter.onCreateViewHolder")
|
beginTraceSession("MyAdapter.onCreateViewHolder")
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
@ -162,7 +172,6 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
|||||||
return sectionOffset
|
return sectionOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
beginTraceSession("MyAdapter.onBindViewHolder")
|
beginTraceSession("MyAdapter.onBindViewHolder")
|
||||||
dataSource?.rawData?.categories?.let { categories ->
|
dataSource?.rawData?.categories?.let { categories ->
|
||||||
@ -174,13 +183,36 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
|||||||
val sectionOffset = getSectionOffset(sectionNumber)
|
val sectionOffset = getSectionOffset(sectionNumber)
|
||||||
val emoji = sectionMojis[position - sectionOffset]
|
val emoji = sectionMojis[position - sectionOffset]
|
||||||
val item = dataSource!!.rawData!!.emojis[emoji]!!.emojiString()
|
val item = dataSource!!.rawData!!.emojis[emoji]!!.emojiString()
|
||||||
|
(holder as EmojiViewHolder).data = item
|
||||||
|
if (scrollState != ScrollState.SETTLING || !isFastScroll) {
|
||||||
|
// Log.i("PERF","Bind with draw at position:$position")
|
||||||
holder.bind(item)
|
holder.bind(item)
|
||||||
|
} else {
|
||||||
|
// Log.i("PERF","Bind without draw at position:$position")
|
||||||
|
toUpdateWhenNotBusy.add(item to holder)
|
||||||
|
holder.bind(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
endTraceSession()
|
endTraceSession()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onViewRecycled(holder: ViewHolder) {
|
||||||
|
if (holder is EmojiViewHolder) {
|
||||||
|
holder.data = null
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
toUpdateWhenNotBusy.removeIf { it.second == holder }
|
||||||
|
} else {
|
||||||
|
val index = toUpdateWhenNotBusy.indexOfFirst { it.second == holder }
|
||||||
|
if (index != -1) {
|
||||||
|
toUpdateWhenNotBusy.removeAt(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.onViewRecycled(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
dataSource?.rawData?.categories?.let {
|
dataSource?.rawData?.categories?.let {
|
||||||
var count = /*number of sections*/ it.size
|
var count = /*number of sections*/ it.size
|
||||||
@ -193,17 +225,28 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
|||||||
|
|
||||||
|
|
||||||
abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
abstract fun bind(s: String)
|
abstract fun bind(s: String?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class EmojiViewHolder(itemView: View) : ViewHolder(itemView) {
|
class EmojiViewHolder(itemView: View) : ViewHolder(itemView) {
|
||||||
|
|
||||||
var emojiView: EmojiDrawView = itemView.findViewById(R.id.grid_item_emoji_text)
|
var emojiView: EmojiDrawView = itemView.findViewById(R.id.grid_item_emoji_text)
|
||||||
|
val placeHolder: View = itemView.findViewById(R.id.grid_item_place_holder)
|
||||||
|
|
||||||
|
var data: String? = null
|
||||||
|
|
||||||
override fun bind(s: String) {
|
override fun bind(s: String?) {
|
||||||
emojiView.emoji = s
|
emojiView.emoji = s
|
||||||
|
if (s != null) {
|
||||||
|
emojiView.mLayout = getStaticLayoutForEmoji(s)
|
||||||
|
placeHolder.visibility = View.GONE
|
||||||
|
// emojiView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
emojiView.mLayout = null
|
||||||
|
placeHolder.visibility = View.VISIBLE
|
||||||
|
// emojiView.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,11 +254,10 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
|||||||
|
|
||||||
var textView: TextView = itemView.findViewById(R.id.section_header_textview)
|
var textView: TextView = itemView.findViewById(R.id.section_header_textview)
|
||||||
|
|
||||||
override fun bind(s: String) {
|
override fun bind(s: String?) {
|
||||||
textView.text = s
|
textView.text = s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -230,40 +272,75 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
|
|||||||
Trace.beginSection(sectionName)
|
Trace.beginSection(sectionName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val staticLayoutCache = HashMap<String, StaticLayout>()
|
||||||
|
|
||||||
|
fun getStaticLayoutForEmoji(emoji: String): StaticLayout {
|
||||||
|
var cachedLayout = staticLayoutCache[emoji]
|
||||||
|
if (cachedLayout == null) {
|
||||||
|
cachedLayout = StaticLayout(emoji, EmojiDrawView.tPaint, EmojiDrawView.emojiSize, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
|
||||||
|
staticLayoutCache[emoji] = cachedLayout!!
|
||||||
|
}
|
||||||
|
return cachedLayout!!
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InteractionListener {
|
||||||
|
fun firstVisibleSectionChange(section: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
//privates
|
||||||
|
|
||||||
|
private val scrollListener = object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
|
scrollState = when (newState) {
|
||||||
|
RecyclerView.SCROLL_STATE_IDLE -> ScrollState.IDLE
|
||||||
|
RecyclerView.SCROLL_STATE_SETTLING -> ScrollState.SETTLING
|
||||||
|
RecyclerView.SCROLL_STATE_DRAGGING -> ScrollState.DRAGGING
|
||||||
|
else -> ScrollState.UNKNWON
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO better
|
||||||
|
if (scrollState == ScrollState.IDLE) {
|
||||||
|
//
|
||||||
|
val toUpdate = toUpdateWhenNotBusy.clone() as ArrayList<Pair<String, EmojiViewHolder>>
|
||||||
|
toUpdateWhenNotBusy.clear()
|
||||||
|
toUpdate.chunked(8).forEach {
|
||||||
|
recyclerView.post {
|
||||||
|
val transition = AutoTransition().apply {
|
||||||
|
duration = 150
|
||||||
|
}
|
||||||
|
for (pair in it) {
|
||||||
|
val holder = pair.second
|
||||||
|
if (pair.first == holder.data) {
|
||||||
|
TransitionManager.beginDelayedTransition(holder.itemView as FrameLayout, transition)
|
||||||
|
val data = holder.data
|
||||||
|
holder.bind(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toUpdateWhenNotBusy.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
//Log.i("SCROLL SPEED","scroll speed $dy")
|
||||||
|
isFastScroll = abs(dy) > 50
|
||||||
|
val visible = (recyclerView.layoutManager as GridLayoutManager).findFirstCompletelyVisibleItemPosition()
|
||||||
|
GlobalScope.launch {
|
||||||
|
val section = getSectionForAbsoluteIndex(visible)
|
||||||
|
if (section != currentFirstVisibleSection) {
|
||||||
|
currentFirstVisibleSection = section
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
interactionListener?.firstVisibleSectionChange(currentFirstVisibleSection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// data class SectionsIndex(val dataSource: EmojiDataSource) {
|
|
||||||
// var sectionsIndex: ArrayList<Int> = ArrayList()
|
|
||||||
// var sectionsInfo: ArrayList<Pair<Int, Int>> = ArrayList()
|
|
||||||
// var itemCount = 0
|
|
||||||
//
|
|
||||||
// init {
|
|
||||||
// var sectionOffset = 1
|
|
||||||
// var lastItemInSection = 0
|
|
||||||
// dataSource.rawData?.categories?.let {
|
|
||||||
// for (category in it) {
|
|
||||||
// sectionsIndex.add(sectionOffset - 1)
|
|
||||||
// lastItemInSection = sectionOffset + category.emojis.size - 1
|
|
||||||
// sectionsInfo.add(sectionOffset to lastItemInSection)
|
|
||||||
// sectionOffset = lastItemInSection + 2
|
|
||||||
// itemCount += (1 + category.emojis.size)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fun getCount(): Int = this.itemCount
|
|
||||||
//
|
|
||||||
// fun isSection(position: Int): Int? {
|
|
||||||
// return sectionsIndex.indexOf(position)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fun getSectionForAbsoluteIndex(position: Int): Int {
|
|
||||||
// for (i in sectionsIndex.size - 1 downTo 0) {
|
|
||||||
// val sectionOffset = sectionsIndex[i]
|
|
||||||
// if (position >= sectionOffset) {
|
|
||||||
// return i
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return 0
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
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
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/tabs"
|
android:id="@+id/tabs"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="30dp"
|
android:layout_height="40dp"
|
||||||
android:background="?attr/colorPrimary"
|
android:background="?attr/colorPrimary"
|
||||||
android:elevation="4dp" />
|
android:elevation="4dp" />
|
||||||
|
|
||||||
|
@ -5,6 +5,16 @@
|
|||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
tools:showIn="@layout/activity_emoji_reaction_picker">
|
tools:showIn="@layout/activity_emoji_reaction_picker">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/grid_item_place_holder"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"
|
||||||
|
android:background="@drawable/circle" />
|
||||||
|
|
||||||
<im.vector.reactions.EmojiDrawView
|
<im.vector.reactions.EmojiDrawView
|
||||||
android:id="@+id/grid_item_emoji_text"
|
android:id="@+id/grid_item_emoji_text"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
|
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:name=".core.services.CallService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileProvider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/riotx_provider_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
@ -18,6 +18,7 @@ package im.vector.riotredesign.core.platform
|
|||||||
|
|
||||||
import com.airbnb.mvrx.BaseMvRxViewModel
|
import com.airbnb.mvrx.BaseMvRxViewModel
|
||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import im.vector.riotredesign.BuildConfig
|
||||||
|
|
||||||
abstract class VectorViewModel<S : MvRxState>(initialState: S)
|
abstract class VectorViewModel<S : MvRxState>(initialState: S)
|
||||||
: BaseMvRxViewModel<S>(initialState, debugMode = false)
|
: BaseMvRxViewModel<S>(initialState, debugMode = BuildConfig.DEBUG)
|
@ -238,3 +238,26 @@ fun openMedia(activity: Activity, savedMediaPath: String, mimeType: String) {
|
|||||||
activity.toast(R.string.error_no_external_application_found)
|
activity.toast(R.string.error_no_external_application_found)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
|
||||||
|
|
||||||
|
var mediaUri: Uri? = null
|
||||||
|
try {
|
||||||
|
mediaUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", file)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("onMediaAction Selected File cannot be shared " + e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (null != mediaUri) {
|
||||||
|
val sendIntent = Intent()
|
||||||
|
sendIntent.action = Intent.ACTION_SEND
|
||||||
|
// Grant temporary read permission to the content URI
|
||||||
|
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
sendIntent.type = mediaMimeType
|
||||||
|
sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri)
|
||||||
|
|
||||||
|
context.startActivity(sendIntent)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -76,11 +76,13 @@ fun requestDisablingBatteryOptimization(activity: Activity, fragment: Fragment?,
|
|||||||
* @param context the context
|
* @param context the context
|
||||||
* @param text the text to copy
|
* @param text the text to copy
|
||||||
*/
|
*/
|
||||||
fun copyToClipboard(context: Context, text: CharSequence) {
|
fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = true) {
|
||||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
clipboard.primaryClip = ClipData.newPlainText("", text)
|
clipboard.primaryClip = ClipData.newPlainText("", text)
|
||||||
|
if (showToast) {
|
||||||
context.toast(R.string.copied_to_clipboard)
|
context.toast(R.string.copied_to_clipboard)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the device locale
|
* Provides the device locale
|
||||||
|
@ -33,7 +33,7 @@ import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
|
|||||||
import io.reactivex.rxkotlin.subscribeBy
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
|
|
||||||
class EmptyState : MvRxState
|
data class EmptyState(val isEmpty: Boolean = true) : MvRxState
|
||||||
|
|
||||||
class HomeActivityViewModel(state: EmptyState,
|
class HomeActivityViewModel(state: EmptyState,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
|
@ -20,16 +20,28 @@ import android.app.Activity.RESULT_OK
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||||
|
import com.airbnb.mvrx.MvRx
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.github.piasy.biv.BigImageViewer
|
||||||
|
import com.github.piasy.biv.loader.ImageLoader
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.jaiselrahman.filepicker.activity.FilePickerActivity
|
import com.jaiselrahman.filepicker.activity.FilePickerActivity
|
||||||
import com.jaiselrahman.filepicker.config.Configurations
|
import com.jaiselrahman.filepicker.config.Configurations
|
||||||
import com.jaiselrahman.filepicker.model.MediaFile
|
import com.jaiselrahman.filepicker.model.MediaFile
|
||||||
@ -41,8 +53,10 @@ import im.vector.matrix.android.api.session.room.model.message.MessageAudioConte
|
|||||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
|
import im.vector.reactions.EmojiReactionPickerActivity
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.dialogs.DialogListItem
|
import im.vector.riotredesign.core.dialogs.DialogListItem
|
||||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||||
@ -62,7 +76,11 @@ import im.vector.riotredesign.features.home.room.detail.composer.TextComposerAct
|
|||||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
|
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
|
||||||
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState
|
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.timeline.action.ActionsHandler
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.timeline.action.MessageMenuViewModel
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.riotredesign.features.html.PillImageSpan
|
import im.vector.riotredesign.features.html.PillImageSpan
|
||||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||||
import im.vector.riotredesign.features.media.ImageMediaViewerActivity
|
import im.vector.riotredesign.features.media.ImageMediaViewerActivity
|
||||||
@ -75,6 +93,7 @@ import org.koin.android.scope.ext.android.bindScope
|
|||||||
import org.koin.android.scope.ext.android.getOrCreateScope
|
import org.koin.android.scope.ext.android.getOrCreateScope
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@ -88,7 +107,10 @@ private const val CAMERA_VALUE_TITLE = "attachment"
|
|||||||
private const val REQUEST_FILES_REQUEST_CODE = 0
|
private const val REQUEST_FILES_REQUEST_CODE = 0
|
||||||
private const val TAKE_IMAGE_REQUEST_CODE = 1
|
private const val TAKE_IMAGE_REQUEST_CODE = 1
|
||||||
|
|
||||||
class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback {
|
class RoomDetailFragment :
|
||||||
|
VectorBaseFragment(),
|
||||||
|
TimelineEventController.Callback,
|
||||||
|
AutocompleteUserPresenter.Callback {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@ -115,8 +137,11 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_room_detail
|
override fun getLayoutResId() = R.layout.fragment_room_detail
|
||||||
|
|
||||||
|
lateinit var actionViewModel: ActionsHandler
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
||||||
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
|
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupToolbar()
|
setupToolbar()
|
||||||
@ -125,6 +150,10 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
roomDetailViewModel.subscribe { renderState(it) }
|
roomDetailViewModel.subscribe { renderState(it) }
|
||||||
textComposerViewModel.subscribe { renderTextComposerState(it) }
|
textComposerViewModel.subscribe { renderTextComposerState(it) }
|
||||||
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
|
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
|
||||||
|
|
||||||
|
actionViewModel.actionCommandEvent.observe(this, Observer {
|
||||||
|
handleActions(it)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
@ -136,7 +165,6 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
|
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
|
||||||
@ -401,9 +429,88 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
vectorBaseActivity.notImplemented()
|
vectorBaseActivity.notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// AutocompleteUserPresenter.Callback
|
||||||
|
|
||||||
override fun onQueryUsers(query: CharSequence?) {
|
override fun onQueryUsers(query: CharSequence?) {
|
||||||
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleActions(it: LiveEvent<ActionsHandler.ActionData>?) {
|
||||||
|
it?.getContentIfNotHandled()?.let { actionData ->
|
||||||
|
|
||||||
|
when (actionData.actionId) {
|
||||||
|
MessageMenuViewModel.ACTION_ADD_REACTION -> {
|
||||||
|
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext()), 0)
|
||||||
|
}
|
||||||
|
MessageMenuViewModel.ACTION_COPY -> {
|
||||||
|
//I need info about the current selected message :/
|
||||||
|
copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false)
|
||||||
|
val snack = Snackbar.make(view!!, requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
|
||||||
|
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
|
||||||
|
snack.show()
|
||||||
|
}
|
||||||
|
MessageMenuViewModel.ACTION_SHARE -> {
|
||||||
|
//TODO current data communication is too limited
|
||||||
|
//Need to now the media type
|
||||||
|
actionData.data?.toString()?.let {
|
||||||
|
//TODO bad, just POC
|
||||||
|
BigImageViewer.imageLoader().loadImage(
|
||||||
|
actionData.hashCode(),
|
||||||
|
Uri.parse(it),
|
||||||
|
object : ImageLoader.Callback {
|
||||||
|
override fun onFinish() {}
|
||||||
|
|
||||||
|
override fun onSuccess(image: File?) {
|
||||||
|
if (image != null)
|
||||||
|
shareMedia(requireContext(), image!!, "image/*")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFail(error: Exception?) {}
|
||||||
|
|
||||||
|
override fun onCacheHit(imageType: Int, image: File?) {}
|
||||||
|
|
||||||
|
override fun onCacheMiss(imageType: Int, image: File?) {}
|
||||||
|
|
||||||
|
override fun onProgress(progress: Int) {}
|
||||||
|
|
||||||
|
override fun onStart() {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MessageMenuViewModel.VIEW_SOURCE,
|
||||||
|
MessageMenuViewModel.VIEW_DECRYPTED_SOURCE -> {
|
||||||
|
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
|
||||||
|
view.findViewById<TextView>(R.id.event_content_text_view)?.let {
|
||||||
|
it.text = actionData.data?.toString() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog.Builder(requireActivity())
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,27 +26,17 @@ import com.airbnb.epoxy.EpoxyController
|
|||||||
import com.airbnb.epoxy.EpoxyModel
|
import com.airbnb.epoxy.EpoxyModel
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotredesign.core.epoxy.LoadingItemModel_
|
import im.vector.riotredesign.core.epoxy.LoadingItemModel_
|
||||||
import im.vector.riotredesign.core.extensions.localDateTime
|
import im.vector.riotredesign.core.extensions.localDateTime
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
|
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.*
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineAsyncHelper
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.canBeMerged
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.nextDisplayableEvent
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.prevSameTypeEvents
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem
|
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_
|
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MergedHeaderItem
|
import im.vector.riotredesign.features.home.room.detail.timeline.item.MergedHeaderItem
|
||||||
|
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||||
import org.threeten.bp.LocalDateTime
|
import org.threeten.bp.LocalDateTime
|
||||||
@ -64,6 +54,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
|
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
|
||||||
fun onFileMessageClicked(messageFileContent: MessageFileContent)
|
fun onFileMessageClicked(messageFileContent: MessageFileContent)
|
||||||
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
|
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
|
||||||
|
fun onEventLongClicked(eventId: String, informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
private val collapsedEventIds = linkedSetOf<String>()
|
private val collapsedEventIds = linkedSetOf<String>()
|
||||||
|
@ -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.permalinks.MatrixPermalinkSpan
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||||
@ -40,15 +34,7 @@ import im.vector.riotredesign.core.resources.ColorProvider
|
|||||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem
|
import im.vector.riotredesign.features.home.room.detail.timeline.item.*
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem_
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageFileItem
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageFileItem_
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem_
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem
|
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem_
|
|
||||||
import im.vector.riotredesign.features.html.EventHtmlRenderer
|
import im.vector.riotredesign.features.html.EventHtmlRenderer
|
||||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||||
@ -93,11 +79,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
memberName = formattedMemberName,
|
memberName = formattedMemberName,
|
||||||
showInformation = showInformation)
|
showInformation = showInformation)
|
||||||
|
|
||||||
|
// val all = event.root.toContent()
|
||||||
|
// val ev = all.toModel<Event>()
|
||||||
return when (messageContent) {
|
return when (messageContent) {
|
||||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback)
|
is MessageEmoteContent -> buildEmoteMessageItem(eventId, messageContent, informationData, callback)
|
||||||
is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback)
|
is MessageTextContent -> buildTextMessageItem(eventId, event.sendState, messageContent, informationData, callback)
|
||||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
|
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
|
||||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback)
|
is MessageNoticeContent -> buildNoticeMessageItem(eventId, messageContent, informationData, callback)
|
||||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback)
|
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback)
|
||||||
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback)
|
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback)
|
||||||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback)
|
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback)
|
||||||
@ -179,7 +167,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
|
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildTextMessageItem(messageContent: MessageTextContent,
|
private fun buildTextMessageItem(eventId: String, sendState: SendState,
|
||||||
|
messageContent: MessageTextContent,
|
||||||
informationData: MessageInformationData,
|
informationData: MessageInformationData,
|
||||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||||
|
|
||||||
@ -191,9 +180,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.message(linkifiedBody)
|
.message(linkifiedBody)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
|
.longClickListener { view ->
|
||||||
|
return@longClickListener callback?.onEventLongClicked(eventId, informationData, messageContent, view)
|
||||||
|
?: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent,
|
private fun buildNoticeMessageItem(eventId: String, messageContent: MessageNoticeContent,
|
||||||
informationData: MessageInformationData,
|
informationData: MessageInformationData,
|
||||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||||
|
|
||||||
@ -208,9 +201,13 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.message(message)
|
.message(message)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
|
.longClickListener { view ->
|
||||||
|
return@longClickListener callback?.onEventLongClicked(eventId, informationData, messageContent, view)
|
||||||
|
?: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent,
|
private fun buildEmoteMessageItem(eventId: String, messageContent: MessageEmoteContent,
|
||||||
informationData: MessageInformationData,
|
informationData: MessageInformationData,
|
||||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||||
|
|
||||||
@ -221,6 +218,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.message(message)
|
.message(message)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
|
.longClickListener { view ->
|
||||||
|
return@longClickListener callback?.onEventLongClicked(eventId, informationData, messageContent, view)
|
||||||
|
?: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): Spannable {
|
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): Spannable {
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
||||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||||
@ -27,6 +28,9 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : VectorEpoxyModel<H>()
|
|||||||
|
|
||||||
abstract val informationData: MessageInformationData
|
abstract val informationData: MessageInformationData
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var longClickListener: View.OnLongClickListener? = null
|
||||||
|
|
||||||
override fun bind(holder: H) {
|
override fun bind(holder: H) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
if (informationData.showInformation) {
|
if (informationData.showInformation) {
|
||||||
@ -41,6 +45,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : VectorEpoxyModel<H>()
|
|||||||
holder.memberNameView.visibility = View.GONE
|
holder.memberNameView.visibility = View.GONE
|
||||||
holder.timeView.visibility = View.GONE
|
holder.timeView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
holder.view.setOnLongClickListener(longClickListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun View.renderSendState() {
|
protected fun View.renderSendState() {
|
||||||
|
@ -18,6 +18,10 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item
|
|||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
data class MessageInformationData(
|
data class MessageInformationData(
|
||||||
val eventId: String,
|
val eventId: String,
|
||||||
val senderId: String,
|
val senderId: String,
|
||||||
@ -26,4 +30,4 @@ data class MessageInformationData(
|
|||||||
val avatarUrl: String?,
|
val avatarUrl: String?,
|
||||||
val memberName: CharSequence? = null,
|
val memberName: CharSequence? = null,
|
||||||
val showInformation: Boolean = true
|
val showInformation: Boolean = true
|
||||||
)
|
) : Parcelable
|
@ -46,6 +46,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||||||
null)
|
null)
|
||||||
holder.messageView.setTextFuture(textFuture)
|
holder.messageView.setTextFuture(textFuture)
|
||||||
holder.messageView.renderSendState()
|
holder.messageView.renderSendState()
|
||||||
|
holder.messageView.setOnLongClickListener(longClickListener)
|
||||||
findPillsAndProcess { it.bind(holder.messageView) }
|
findPillsAndProcess { it.bind(holder.messageView) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
@ -33,9 +34,14 @@ abstract class NoticeItem : VectorEpoxyModel<NoticeItem.Holder>() {
|
|||||||
@EpoxyAttribute var userId: String = ""
|
@EpoxyAttribute var userId: String = ""
|
||||||
@EpoxyAttribute var memberName: CharSequence? = null
|
@EpoxyAttribute var memberName: CharSequence? = null
|
||||||
|
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var longClickListener: View.OnLongClickListener? = null
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
holder.noticeTextView.text = noticeText
|
holder.noticeTextView.text = noticeText
|
||||||
AvatarRenderer.render(avatarUrl, userId, memberName?.toString(), holder.avatarImageView)
|
AvatarRenderer.render(avatarUrl, userId, memberName?.toString(), holder.avatarImageView)
|
||||||
|
holder.view.setOnLongClickListener(longClickListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Holder : VectorEpoxyHolder() {
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
@ -160,6 +160,17 @@ object ThemeUtils {
|
|||||||
return matchedColor
|
return matchedColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAttribute(c: Context, @AttrRes attribute: Int): TypedValue? {
|
||||||
|
try {
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
c.theme.resolveAttribute(attribute, typedValue, true)
|
||||||
|
return typedValue
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Unable to get color")
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the resource Id applied to the current theme
|
* Get the resource Id applied to the current theme
|
||||||
*
|
*
|
||||||
|
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"
|
android:layout_height="match_parent"
|
||||||
tools:openDrawer="start">
|
tools:openDrawer="start">
|
||||||
|
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
android:id="@+id/coordinatorLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/homeDetailFragmentContainer"
|
android:id="@+id/homeDetailFragmentContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
|
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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
android:paddingLeft="16dp"
|
android:paddingLeft="16dp"
|
||||||
android:paddingRight="16dp">
|
android:paddingRight="16dp">
|
||||||
|
|
||||||
@ -54,6 +55,7 @@
|
|||||||
android:id="@+id/messageTextView"
|
android:id="@+id/messageTextView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:foreground="?attr/selectableItemBackgroundBorderless"
|
||||||
android:layout_marginStart="64dp"
|
android:layout_marginStart="64dp"
|
||||||
android:layout_marginLeft="64dp"
|
android:layout_marginLeft="64dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
|
@ -57,6 +57,8 @@
|
|||||||
<string name="stay">Stay</string>
|
<string name="stay">Stay</string>
|
||||||
<string name="send">Send</string>
|
<string name="send">Send</string>
|
||||||
<string name="copy">Copy</string>
|
<string name="copy">Copy</string>
|
||||||
|
<string name="edit">Edit</string>
|
||||||
|
<string name="reply">Reply</string>
|
||||||
<string name="resend">Resend</string>
|
<string name="resend">Resend</string>
|
||||||
<string name="redact">Remove</string>
|
<string name="redact">Remove</string>
|
||||||
<string name="quote">Quote</string>
|
<string name="quote">Quote</string>
|
||||||
@ -1400,4 +1402,9 @@ Why choose Riot.im?
|
|||||||
<string name="autodiscover_well_known_autofill_dialog_message">Riot detected a custom server configuration for your userId domain \"%s\":\n%s</string>
|
<string name="autodiscover_well_known_autofill_dialog_message">Riot detected a custom server configuration for your userId domain \"%s\":\n%s</string>
|
||||||
<string name="autodiscover_well_known_autofill_confirm">Use Config</string>
|
<string name="autodiscover_well_known_autofill_confirm">Use Config</string>
|
||||||
|
|
||||||
|
|
||||||
|
<string name="reactions_agree">Agree</string>
|
||||||
|
<string name="reactions_like">Like</string>
|
||||||
|
<string name="message_add_reaction">Add Reaction</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
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…
Reference in New Issue
Block a user