я не помню, что тут

This commit is contained in:
2021-10-02 20:13:27 +03:00
parent 7c72199d32
commit 9e074dd5ad
35 changed files with 945 additions and 293 deletions
+7 -2
View File
@@ -7,9 +7,9 @@ plugins {
id("com.android.application")
id("kotlin-android")
id("kotlin-kapt")
id("kotlin-parcelize")
id("androidx.navigation.safeargs.kotlin")
id("dagger.hilt.android.plugin")
id("kotlin-parcelize")
}
android {
@@ -38,6 +38,9 @@ android {
getByName("release") {
isMinifyEnabled = false
buildConfigField("String", "vkLogin", login)
buildConfigField("String", "vkPassword", password)
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -68,7 +71,7 @@ android {
kapt {
correctErrorTypes = true
//use this shit if you don't want to have hilt errors
//use this shit if you don't want have hilt errors
javacOptions {
option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true")
}
@@ -83,6 +86,8 @@ dependencies {
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.paging:paging-runtime-ktx:3.0.1")
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
implementation("com.google.android.material:material:1.5.0-alpha03")
implementation("androidx.core:core-ktx:1.7.0-beta01")
@@ -6,6 +6,7 @@ import com.meloda.fast.common.AppGlobal
object UserConfig {
private const val FAST_TOKEN = "fast_token"
private const val TOKEN = "token"
private const val USER_ID = "user_id"
@@ -25,8 +26,16 @@ object UserConfig {
AppGlobal.preferences.edit().putString(TOKEN, value).apply()
}
var fastToken: String = ""
get() = AppGlobal.preferences.getString(FAST_TOKEN, "") ?: ""
set(value) {
field = value
AppGlobal.preferences.edit().putString(FAST_TOKEN, value).apply()
}
fun clear() {
accessToken = ""
fastToken = ""
userId = -1
}
@@ -7,6 +7,8 @@ object VKConstants {
const val USER_FIELDS =
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info"
const val ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS"
const val API_VERSION = "5.132"
const val VK_APP_ID = "2274003"
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
@@ -42,6 +42,12 @@ object VkUtils {
return forwards
}
fun parseReplyMessage(baseReplyMessage: BaseVkMessage?): VkMessage? {
if (baseReplyMessage == null) return null
return baseReplyMessage.asVkMessage()
}
fun parseAttachments(baseAttachments: List<BaseVkAttachmentItem>?): List<VkAttachment>? {
if (baseAttachments.isNullOrEmpty()) return null
@@ -1,9 +1,12 @@
package com.meloda.fast.api.model
import android.os.Parcelable
import androidx.lifecycle.MutableLiveData
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.meloda.fast.api.model.attachments.VkAttachment
import com.meloda.fast.base.adapter.SelectableItem
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Entity(tableName = "messages")
@@ -24,9 +27,21 @@ data class VkMessage(
val actionMessage: String? = null,
val geoType: String? = null,
val important: Boolean = false,
var forwards: List<VkMessage>? = null,
var attachments: List<VkAttachment>? = null
) : Parcelable {
var attachments: List<VkAttachment>? = null,
// @Embedded(prefix = "replyMessage_")
var replyMessage: VkMessage? = null
) : SelectableItem() {
@Ignore
@IgnoredOnParcel
val user = MutableLiveData<VkUser?>()
@Ignore
@IgnoredOnParcel
val group = MutableLiveData<VkGroup?>()
fun isPeerChat() = peerId > 2_000_000_000
@@ -1,8 +1,10 @@
package com.meloda.fast.api.model.attachments
import androidx.room.Ignore
import com.meloda.fast.api.model.base.attachments.BaseVkPhoto
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize
data class VkPhoto(
@@ -17,13 +19,58 @@ data class VkPhoto(
val userId: Int?
) : VkAttachment() {
@Ignore
@IgnoredOnParcel
private val sizesChars = Stack<Char>()
init {
sizesChars.push('s')
sizesChars.push('m')
sizesChars.push('x')
sizesChars.push('o')
sizesChars.push('p')
sizesChars.push('q')
sizesChars.push('r')
sizesChars.push('y')
sizesChars.push('z')
sizesChars.push('w')
}
@IgnoredOnParcel
val className: String = this::class.java.name
fun sizeOfType(type: Char): BaseVkPhoto.Size? {
fun getMaxSize(): BaseVkPhoto.Size? {
return getSizeOrSmaller(sizesChars.peek())
}
fun getSizeOrNull(type: Char): BaseVkPhoto.Size? {
for (size in sizes) {
if (size.type == type.toString())
if (size.type == type.toString()) return size
}
return null
}
fun getSizeOrSmaller(type: Char): BaseVkPhoto.Size? {
val photoStack = sizesChars.clone() as Stack<Char>
val sizeIndex = photoStack.search(type)
if (sizeIndex == -1) return null
for (i in 0 until sizeIndex) {
photoStack.pop()
}
for (i in 0 until photoStack.size) {
val size = getSizeOrNull(photoStack.peek())
if (size == null) {
photoStack.pop()
continue
} else {
return size
}
}
return null
@@ -23,7 +23,8 @@ data class BaseVkMessage(
val payload: String,
val geo: Geo?,
val action: Action?,
val ttl: Int
val ttl: Int,
val reply_message: BaseVkMessage?
) : Parcelable {
fun asVkMessage() = VkMessage(
@@ -44,6 +45,7 @@ data class BaseVkMessage(
).also {
it.attachments = VkUtils.parseAttachments(attachments)
it.forwards = VkUtils.parseForwards(fwd_messages)
it.replyMessage = VkUtils.parseReplyMessage(reply_message)
}
@Parcelize
@@ -17,6 +17,7 @@ class AuthInterceptor : Interceptor {
builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8"))
}
// TODO: 9/29/2021 crash on timeout
return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build())
}
@@ -114,7 +114,7 @@ abstract class BaseAdapter<Item, VH : BaseHolder>(
holder.bind(position)
}
protected fun initListeners(itemView: View, position: Int) {
protected open fun initListeners(itemView: View, position: Int) {
if (itemView is AdapterView<*>) return
itemView.setOnClickListener { itemClickListener.invoke(position) }
@@ -0,0 +1,15 @@
package com.meloda.fast.base.adapter
import android.os.Parcelable
import androidx.room.Ignore
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
open class SelectableItem : Parcelable {
@Ignore
@IgnoredOnParcel
var isSelected: Boolean = false
}
@@ -19,7 +19,7 @@ import com.meloda.fast.database.dao.UsersDao
VkUser::class,
VkGroup::class
],
version = 24,
version = 25,
exportSchema = false,
)
@TypeConverters(Converters::class)
@@ -1,6 +1,8 @@
package com.meloda.fast.extensions
import android.graphics.*
import android.view.View
import androidx.core.view.isVisible
import kotlin.math.min
fun Bitmap.borderedCircularBitmap(
@@ -71,3 +73,5 @@ fun Bitmap.borderedCircularBitmap(
diameter // height
)
}
val View.isNotVisible get() = !isVisible
@@ -1,25 +1,31 @@
package com.meloda.fast.screens.conversations
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.viewbinding.library.fragment.viewBinding
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.datastore.preferences.core.edit
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import coil.load
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.meloda.fast.R
import com.meloda.fast.activity.MainActivity
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.base.BaseViewModelFragment
import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.AppSettings
import com.meloda.fast.common.dataStore
import com.meloda.fast.databinding.FragmentConversationsBinding
@@ -29,6 +35,7 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.roundToInt
@AndroidEntryPoint
class ConversationsFragment :
@@ -53,6 +60,24 @@ class ConversationsFragment :
}
}
private val avatarPopupMenu: PopupMenu
get() =
PopupMenu(
requireContext(),
binding.avatar,
Gravity.BOTTOM
).apply {
menu.add(getString(R.string.log_out))
setOnMenuItemClickListener { item ->
if (item.title == getString(R.string.log_out)) {
showLogOutDialog()
return@setOnMenuItemClickListener true
}
false
}
}
private var isPaused = false
private var isExpanded = true
@@ -74,11 +99,7 @@ class ConversationsFragment :
}.collect { }
}
binding.createChat.setOnClickListener {
Snackbar.make(it, "Test snackbar", Snackbar.LENGTH_SHORT)
.setAction("Action") {}
.show()
}
binding.createChat.setOnClickListener {}
UserConfig.vkUser.observe(viewLifecycleOwner) {
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } }
@@ -87,28 +108,49 @@ class ConversationsFragment :
binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
if (isPaused) return@OnOffsetChangedListener
if (verticalOffset <= -100) {
binding.avatarContainer.alpha = 0f
return@OnOffsetChangedListener
}
val alpha = 1 - abs(verticalOffset * 0.01).toFloat()
// if (verticalOffset <= -100) {
// binding.avatarContainer.alpha = 0f
// return@OnOffsetChangedListener
// }
binding.avatarContainer.alpha = alpha
// from 0 to -294
// from 0 to 29
// if -147
// 30 - value
var value = 30 - (abs(verticalOffset) * 0.1).roundToInt()
val bottomPadding = 0
// if (verticalOffset > -150) AndroidUtils.px(30).roundToInt()
// else (30 + abs(verticalOffset) * 0.1).roundToInt()
val endPadding = 0
// if (verticalOffset > 30) 30
// else (abs(verticalOffset) * 0.1).roundToInt()
binding.avatarContainer.updatePadding(
bottom = value,
right = endPadding
)
println("Fast::ConversationsFragment::onOffset verticalOffset = $verticalOffset; bottomPadding = $value; endPadding = $endPadding")
// binding.avatarContainer.alpha = alpha
})
if (isPaused) {
isPaused = false
return
}
binding.toolbar.overflowIcon = ContextCompat.getDrawable(requireContext(), R.drawable.test)
viewModel.loadProfileUser()
viewModel.loadConversations()
binding.avatar.setOnClickListener {
lifecycleScope.launchWhenResumed {
avatarPopupMenu.show()
}
binding.avatar.setOnLongClickListener {
lifecycleScope.launch {
requireContext().dataStore.edit { settings ->
val isMultilineEnabled = settings[AppSettings.keyIsMultilineEnabled] ?: true
settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled
@@ -117,7 +159,30 @@ class ConversationsFragment :
adapter.notifyItemRangeChanged(0, adapter.itemCount)
}
}
true
}
if (isPaused) {
isPaused = false
return
}
viewModel.loadProfileUser()
viewModel.loadConversations()
}
private fun showLogOutDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.confirm)
.setMessage(R.string.log_out_confirm)
.setPositiveButton(R.string.yes) { _, _ ->
UserConfig.clear()
AppGlobal.appDatabase.clearAllTables()
requireActivity().finishAffinity()
requireActivity().startActivity(Intent(requireContext(), MainActivity::class.java))
}
.setNegativeButton(R.string.no, null)
.show()
}
override fun onEvent(event: VKEvent) {
@@ -3,13 +3,13 @@ package com.meloda.fast.screens.conversations
import androidx.lifecycle.viewModelScope
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.network.datasource.ConversationsDataSource
import com.meloda.fast.api.network.datasource.UsersDataSource
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.request.ConversationsGetRequest
import com.meloda.fast.api.model.request.UsersGetRequest
import com.meloda.fast.api.network.datasource.ConversationsDataSource
import com.meloda.fast.api.network.datasource.UsersDataSource
import com.meloda.fast.base.viewmodel.BaseViewModel
import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent
@@ -26,13 +26,15 @@ class ConversationsViewModel @Inject constructor(
private val usersDataSource: UsersDataSource
) : BaseViewModel() {
fun loadConversations() = viewModelScope.launch(Dispatchers.Default) {
fun loadConversations(
offset: Int? = null
) = viewModelScope.launch(Dispatchers.Default) {
makeJob({
dataSource.getAllChats(
ConversationsGetRequest(
count = 30,
// offset = 177,
extended = true,
offset = offset,
fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}"
)
)
@@ -52,6 +54,7 @@ class ConversationsViewModel @Inject constructor(
sendEvent(
ConversationsLoaded(
count = response.count,
offset = offset,
unreadCount = response.unreadCount ?: 0,
conversations = response.items.map { items ->
items.conversation.asVkConversation(
@@ -73,9 +76,7 @@ class ConversationsViewModel @Inject constructor(
}
fun loadProfileUser() = viewModelScope.launch {
makeJob({
usersDataSource.getById(UsersGetRequest(fields = "online,photo_200"))
},
makeJob({ usersDataSource.getById(UsersGetRequest(fields = VKConstants.USER_FIELDS)) },
onAnswer = {
it.response?.let { r ->
val users = r.map { u -> u.asVkUser() }
@@ -89,6 +90,7 @@ class ConversationsViewModel @Inject constructor(
data class ConversationsLoaded(
val count: Int,
val offset: Int?,
val unreadCount: Int?,
val conversations: List<VkConversation>,
val profiles: HashMap<Int, VkUser>,
@@ -1,11 +1,17 @@
package com.meloda.fast.screens.login
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.Typeface
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.viewbinding.library.fragment.viewBinding
import android.webkit.CookieManager
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
@@ -19,6 +25,8 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import com.meloda.fast.BuildConfig
import com.meloda.fast.R
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants
import com.meloda.fast.base.BaseViewModelFragment
import com.meloda.fast.base.viewmodel.*
import com.meloda.fast.databinding.DialogCaptchaBinding
@@ -29,7 +37,10 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jsoup.Jsoup
import java.net.URLEncoder
import java.util.*
import java.util.regex.Pattern
import kotlin.concurrent.schedule
@AndroidEntryPoint
@@ -89,11 +100,91 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
}
private fun prepareViews() {
prepareWebView()
prepareEmailEditText()
preparePasswordEditText()
prepareAuthButton()
}
@SuppressLint("SetJavaScriptEnabled")
private fun prepareWebView() {
with(binding.webView) {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
clearCache(true)
webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
parseAuthUrl(url)
}
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
val a = Jsoup.parse(url)
val b = 0
}
}
}
CookieManager.getInstance().apply {
removeAllCookies(null)
flush()
setAcceptCookie(true)
}
}
private fun launchWebView() {
binding.webView.loadUrl(
"https://oauth.vk.com/authorize?client_id=${UserConfig.FAST_APP_ID}&" +
"display=mobile&scope=136297695&" +
"redirect_uri=${
URLEncoder.encode(
"https://oauth.vk.com/blank.html",
Charsets.UTF_8.toString()
)
}&response_type=token&v=${VKConstants.API_VERSION}"
)
}
private fun parseAuthUrl(url: String) {
if (url.isBlank()) return
if (url.startsWith("https://oauth.vk.com/blank.html")) {
if (url.contains("error")) {
Log.e("Fast::Login", "errorUrl: $url")
return
}
val authData = parseRedirectUrl(url)
if (authData == null) {
Log.e("Fast::Login", "errorUrl: $url")
return
}
val token = authData.first
UserConfig.fastToken = token
}
}
private fun parseRedirectUrl(url: String): Pair<String, Int>? {
val accessToken = extractPattern(url, "access_token=(.*?)&") ?: return null
val userId = extractPattern(url, "id=(\\d*)")?.toIntOrNull() ?: return null
return accessToken to userId
}
private fun extractPattern(string: String, pattern: String): String? {
val p = Pattern.compile(pattern)
val m = p.matcher(string)
return if (m.find()) {
m.group(1)
} else null
}
private fun prepareEmailEditText() {
binding.loginInput.addTextChangedListener {
if (!binding.loginLayout.error.isNullOrBlank()) binding.loginLayout.error = ""
@@ -296,6 +387,8 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
private fun goToMain(haveAuthorized: Boolean) = lifecycleScope.launch {
if (haveAuthorized) delay(500)
launchWebView()
findNavController().navigate(R.id.toMain)
}
@@ -30,6 +30,7 @@ import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.roundToInt
// TODO: 9/29/2021 use recyclerview for viewing attachments
class AttachmentInflater constructor(
private val context: Context,
private val container: LinearLayoutCompat,
@@ -94,12 +95,15 @@ class AttachmentInflater constructor(
}
private fun photo(photo: VkPhoto) {
val size = photo.sizeOfType('m') ?: return
val size = photo.getSizeOrSmaller('y') ?: return
val newPhoto = ShapeableImageView(context).apply {
layoutParams = LinearLayoutCompat.LayoutParams(
AndroidUtils.px(size.width).roundToInt(),
AndroidUtils.px(size.height).roundToInt()
// ViewGroup.LayoutParams.MATCH_PARENT,
size.width,
size.height
// AndroidUtils.px(size.width).roundToInt(),
// AndroidUtils.px(size.height).roundToInt()
)
shapeAppearanceModel =
@@ -222,14 +226,7 @@ class AttachmentInflater constructor(
binding.caption.text = link.caption
binding.caption.isVisible = !link.caption.isNullOrBlank()
binding.preview.shapeAppearanceModel.toBuilder()
.setAllCornerSizes(AndroidUtils.px(20))
.build()
.let {
binding.preview.shapeAppearanceModel = it
}
link.photo?.sizeOfType('m')?.let {
link.photo?.getMaxSize()?.let {
binding.preview.load(it.url) { crossfade(150) }
binding.preview.isVisible = true
return
@@ -245,8 +242,8 @@ class AttachmentInflater constructor(
with(binding.image) {
layoutParams = LinearLayoutCompat.LayoutParams(
AndroidUtils.px(180).roundToInt(),
AndroidUtils.px(180).roundToInt()
AndroidUtils.px(140).roundToInt(),
AndroidUtils.px(140).roundToInt()
)
load(url) { crossfade(150) }
@@ -5,6 +5,7 @@ import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
@@ -29,7 +30,11 @@ class MessagesHistoryAdapter constructor(
val conversation: VkConversation,
val profiles: HashMap<Int, VkUser> = hashMapOf(),
val groups: HashMap<Int, VkGroup> = hashMapOf()
) : BaseAdapter<VkMessage, MessagesHistoryAdapter.Holder>(context, values, COMPARATOR) {
) : BaseAdapter<VkMessage, MessagesHistoryAdapter.BasicHolder>(context, values, COMPARATOR) {
private var highlightTimer: Timer? = null
var onItemClickListener: ((position: Int, view: View) -> Unit)? = null
override fun getItemViewType(position: Int): Int {
when {
@@ -49,7 +54,7 @@ class MessagesHistoryAdapter constructor(
private fun isPositionHeader(position: Int) = position == 0
private fun isPositionFooter(position: Int) = position >= actualSize
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicHolder {
return when (viewType) {
// magick numbers is great!
HEADER -> Header(createEmptyView(60))
@@ -67,6 +72,13 @@ class MessagesHistoryAdapter constructor(
}
}
override fun initListeners(itemView: View, position: Int) {
if (itemView is AdapterView<*>) return
itemView.setOnClickListener { onItemClickListener?.invoke(position, itemView) }
itemView.setOnLongClickListener { itemLongClickListener.invoke(position) }
}
private fun createEmptyView(size: Int) = View(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
@@ -78,22 +90,22 @@ class MessagesHistoryAdapter constructor(
isFocusable = false
}
override fun onBindViewHolder(holder: Holder, position: Int) {
override fun onBindViewHolder(holder: BasicHolder, position: Int) {
if (holder is Header || holder is Footer) return
initListeners(holder.itemView, position)
holder.bind(position)
}
open inner class Holder(v: View = View(context)) : BaseHolder(v)
open inner class BasicHolder(v: View = View(context)) : BaseHolder(v)
inner class Header(v: View) : Holder(v)
inner class Header(v: View) : BasicHolder(v)
inner class Footer(v: View) : Holder(v)
inner class Footer(v: View) : BasicHolder(v)
inner class IncomingMessage(
private val binding: ItemMessageInBinding
) : Holder(binding.root) {
) : BasicHolder(binding.root) {
override fun bind(position: Int) {
val message = getItem(position)
@@ -103,6 +115,9 @@ class MessagesHistoryAdapter constructor(
MessagesPreparator(
context = context,
root = binding.root,
conversation = conversation,
message = message,
prevMessage = prevMessage,
@@ -112,7 +127,6 @@ class MessagesHistoryAdapter constructor(
bubble = binding.bubble,
text = binding.text,
spacer = binding.spacer,
time = binding.time,
unread = binding.unread,
attachmentContainer = binding.attachmentContainer,
attachmentSpacer = binding.attachmentSpacer,
@@ -125,11 +139,7 @@ class MessagesHistoryAdapter constructor(
inner class OutgoingMessage(
private val binding: ItemMessageOutBinding
) : Holder(binding.root) {
init {
binding.bubbleStroke.setOnClickListener { binding.bubble.performClick() }
}
) : BasicHolder(binding.root) {
override fun bind(position: Int) {
val message = getItem(position)
@@ -138,15 +148,14 @@ class MessagesHistoryAdapter constructor(
MessagesPreparator(
context = context,
root = binding.root,
conversation = conversation,
message = message,
prevMessage = prevMessage,
bubble = binding.bubble,
bubbleStroke = binding.bubbleStroke,
text = binding.text,
spacer = binding.spacer,
time = binding.time,
unread = binding.unread,
attachmentContainer = binding.attachmentContainer,
attachmentSpacer = binding.attachmentSpacer,
@@ -159,7 +168,7 @@ class MessagesHistoryAdapter constructor(
inner class ServiceMessage(
private val binding: ItemMessageServiceBinding
) : Holder(binding.root) {
) : BasicHolder(binding.root) {
private val youPrefix = context.getString(R.string.you_message_prefix)
@@ -198,7 +207,7 @@ class MessagesHistoryAdapter constructor(
binding.photo.isVisible = true
val size = attachment.sizeOfType('m') ?: return@let
val size = attachment.getSizeOrSmaller('y') ?: return@let
binding.photo.layoutParams = LinearLayoutCompat.LayoutParams(
size.width,
@@ -213,7 +222,7 @@ class MessagesHistoryAdapter constructor(
}
}
private val actualSize get() = values.size
val actualSize get() = values.size
override fun getItemCount(): Int {
if (actualSize == 0) return 2
@@ -1,12 +1,15 @@
package com.meloda.fast.screens.messages
import android.graphics.Color
import android.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.viewbinding.library.fragment.viewBinding
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData
@@ -16,6 +19,7 @@ import coil.load
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.meloda.fast.R
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
@@ -26,12 +30,14 @@ import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.databinding.FragmentMessagesHistoryBinding
import com.meloda.fast.extensions.TextViewExtensions.clear
import com.meloda.fast.extensions.isNotVisible
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.util.TimeUtils
import dagger.hilt.android.AndroidEntryPoint
import java.text.SimpleDateFormat
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
@AndroidEntryPoint
class MessagesHistoryFragment :
@@ -60,11 +66,14 @@ class MessagesHistoryFragment :
private val adapter: MessagesHistoryAdapter by lazy {
MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also {
it.itemClickListener = this::onItemClick
it.onItemClickListener = this::onItemClick
it.itemLongClickListener = this::onItemLongClick
}
}
private val replyMessage = MutableLiveData<VkMessage?>()
private val isAttachmentPanelVisible = MutableLiveData(false)
private var timestampTimer: Timer? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -98,19 +107,7 @@ class MessagesHistoryFragment :
binding.status.text = status ?: "..."
val avatar = when {
conversation.isChat() -> conversation.photo200
conversation.isUser() -> user?.photo200
conversation.isGroup() -> group?.photo200
else -> null
}
binding.avatar.load(avatar) {
crossfade(false)
error(ColorDrawable(Color.RED))
}
binding.online.isVisible = user?.online == true
prepareAvatar()
prepareViews()
@@ -166,8 +163,12 @@ class MessagesHistoryFragment :
})
binding.message.doAfterTextChanged {
val newValue = if (it.toString().isNotBlank()) Action.SEND
else Action.RECORD
val canSend =
it.toString().isNotBlank()
val newValue =
if (canSend) Action.SEND
else Action.RECORD
if (action.value != newValue) action.value = newValue
}
@@ -195,12 +196,117 @@ class MessagesHistoryFragment :
else -> return@observe
}
}
isAttachmentPanelVisible.observe(viewLifecycleOwner) {
val layoutParams = binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.bottomMargin =
if (it) (binding.attachmentPanel.height / 1.5).roundToInt() else 0
}
hideAttachmentPanel(duration = 1)
binding.avatar.setOnClickListener {
val isShown = binding.attachmentPanel.isVisible
if (isShown) {
hideAttachmentPanel()
} else {
showAttachmentPanel()
}
}
binding.attachmentPanel.setOnClickListener c@{
val message = replyMessage.value ?: return@c
val index = adapter.values.indexOf(message)
if (index == -1) return@c
binding.recyclerView.smoothScrollToPosition(index)
}
binding.dismissReply.setOnClickListener {
if (replyMessage.value != null) replyMessage.value = null
hideAttachmentPanel()
}
}
private fun prepareAvatar() {
val avatar = when {
conversation.ownerId == VKConstants.FAST_GROUP_ID -> null
conversation.isUser() -> user?.photo200
conversation.isGroup() -> group?.photo200
conversation.isChat() -> conversation.photo200
else -> null
}
binding.avatar.isVisible = avatar != null
if (avatar == null) {
binding.avatarPlaceholder.isVisible = true
if (conversation.ownerId == VKConstants.FAST_GROUP_ID) {
binding.placeholderBack.setImageDrawable(
ColorDrawable(
ContextCompat.getColor(requireContext(), R.color.a1_400)
)
)
binding.placeholder.imageTintList =
ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.a1_0))
binding.placeholder.setImageResource(R.drawable.ic_fast_logo)
binding.placeholder.setPadding(18)
} else {
binding.placeholderBack.setImageDrawable(
ColorDrawable(
ContextCompat.getColor(requireContext(), R.color.n1_50)
)
)
binding.placeholder.imageTintList =
ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.n2_500))
binding.placeholder.setImageResource(R.drawable.ic_account_circle_cut)
binding.placeholder.setPadding(0)
binding.avatar.setImageDrawable(null)
}
} else {
binding.avatar.load(avatar) {
crossfade(200)
target {
binding.avatarPlaceholder.isVisible = false
binding.avatar.setImageDrawable(it)
}
}
}
binding.phantomIcon.isVisible = conversation.isPhantom
binding.online.isVisible = user?.online == true
binding.pin.isVisible = conversation.isPinned
}
private fun showAttachmentPanel(duration: Long = 250) {
if (isAttachmentPanelVisible.value == false) isAttachmentPanelVisible.value = true
binding.attachmentPanel.animate()
.translationY(0f)
.alpha(1f)
.setDuration(duration)
.withStartAction { binding.attachmentPanel.isVisible = true }
.start()
}
private fun hideAttachmentPanel(duration: Long = 250) {
if (isAttachmentPanelVisible.value == true) isAttachmentPanelVisible.value = false
binding.attachmentPanel.animate()
.alpha(0f)
.translationY(50f)
.setDuration(duration)
.withEndAction { binding.attachmentPanel.isVisible = false }
.start()
}
private fun performAction() {
if (action.value == Action.RECORD) {
return
} else if (action.value == Action.SEND) {
val messageText = binding.message.text.toString().trim()
if (messageText.isBlank()) return
@@ -214,18 +320,25 @@ class MessagesHistoryFragment :
peerId = conversation.id,
fromId = UserConfig.userId,
date = (date / 1000).toInt(),
randomId = 0
randomId = 0,
replyMessage = replyMessage.value
)
adapter.add(message)
adapter.notifyDataSetChanged()
adapter.notifyItemInserted(adapter.actualSize - 1)
binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
binding.message.clear()
val replyMessage = replyMessage.value
this.replyMessage.value = null
hideAttachmentPanel()
viewModel.sendMessage(
peerId = conversation.id,
message = messageText,
randomId = 0
randomId = 0,
replyTo = replyMessage?.id
) { message = message.copyMessage(id = it) }
}
}
@@ -283,17 +396,22 @@ class MessagesHistoryFragment :
private fun markMessagesAsImportant(event: MessagesMarkAsImportant) {
var changed = false
val positions = mutableListOf<Int>()
for (i in adapter.values.indices) {
val message = adapter.values[i]
if (event.messagesIds.contains(message.id)) {
if (!changed) changed = true
positions.add(i)
adapter.values[i] = message.copyMessage(
important = event.important
)
}
}
if (changed) adapter.notifyDataSetChanged()
if (changed) positions.forEach { adapter.notifyItemChanged(it) }
}
private fun refreshMessages(event: MessagesLoaded) {
@@ -314,21 +432,111 @@ class MessagesHistoryFragment :
else binding.recyclerView.scrollToPosition(adapter.lastPosition)
}
private fun onItemClick(position: Int) {
private fun onItemClick(position: Int, view: View) {
val message = adapter.values[position]
if (message.action != null) return
val important = if (message.important) "Unmark as important" else "Mark as important"
// val popupMenu = PopupMenu(requireContext(), view)
//
// val reply = popupMenu.menu.add(
// getString(R.string.message_context_action_reply)
// )
//
// reply.icon =
// ContextCompat.getDrawable(
// requireContext(),
// R.drawable.ic_attachment_wall_reply
// )?.constantState?.newDrawable()?.also {
// it.setTint(
// ContextCompat.getColor(
// requireContext(),
// R.color.textColorSecondaryVariant
// )
// )
// }
//
// val important = popupMenu.menu.add(
// getString(
// if (message.important) R.string.message_context_action_unmark_as_important
// else R.string.message_context_action_mark_as_important
// )
// )
//
// important.icon =
// ContextCompat.getDrawable(
// requireContext(),
// R.drawable.ic_star_border
// )?.constantState?.newDrawable()?.also {
// it.setTint(
// ContextCompat.getColor(
// requireContext(),
// R.color.textColorSecondaryVariant
// )
// )
// }
//
// popupMenu.setForceShowIcon(true)
// popupMenu.setOnMenuItemClickListener {
// when (it) {
// reply -> {
// val title = when {
// message.isGroup() && message.group.value != null -> message.group.value?.name
// message.isUser() && message.user.value != null -> message.user.value?.fullName
// else -> null
// }
//
// if (replyMessage.value != message) replyMessage.value = message
//
// binding.replyMessageTitle.text = title
// binding.replyMessageText.text = message.text ?: "[no_message]"
//
// if (binding.attachmentPanel.isNotVisible) binding.avatar.performClick()
// true
// }
//
// important -> {
// viewModel.markAsImportant(
// messagesIds = listOf(message.id),
// important = !message.important
// )
// true
// }
//
// else -> false
// }
// }
// popupMenu.show()
val params = arrayOf(important)
val reply = getString(R.string.message_context_action_reply)
val important = getString(
if (message.important) R.string.message_context_action_unmark_as_important
else R.string.message_context_action_mark_as_important
)
val params = arrayOf(reply, important)
val dialog = MaterialAlertDialogBuilder(requireContext())
.setItems(params) { _, which ->
if (which == 0) {
viewModel.markAsImportant(
when (params[which]) {
important -> viewModel.markAsImportant(
messagesIds = listOf(message.id),
important = !message.important
)
reply -> {
val title = when {
message.isGroup() && message.group.value != null -> message.group.value?.name
message.isUser() && message.user.value != null -> message.user.value?.fullName
else -> null
}
if (replyMessage.value != message) replyMessage.value = message
binding.replyMessageTitle.text = title
binding.replyMessageText.text = message.text ?: "[no_message]"
if (binding.attachmentPanel.isNotVisible) binding.avatar.performClick()
}
}
}
@@ -91,6 +91,7 @@ class MessagesHistoryViewModel @Inject constructor(
peerId: Int,
message: String? = null,
randomId: Int = 0,
replyTo: Int? = null,
setId: ((messageId: Int) -> Unit)? = null
) = viewModelScope.launch {
makeJob(
@@ -99,7 +100,8 @@ class MessagesHistoryViewModel @Inject constructor(
MessagesSendRequest(
peerId = peerId,
randomId = randomId,
message = message
message = message,
replyTo = replyTo
)
)
},
@@ -1,6 +1,7 @@
package com.meloda.fast.screens.messages
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.util.Log
import android.view.View
import android.widget.ImageView
@@ -10,7 +11,6 @@ import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import coil.load
import com.meloda.fast.R
import com.meloda.fast.api.VkUtils
@@ -20,7 +20,6 @@ import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.attachments.VkSticker
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.widget.BoundedLinearLayout
import java.text.SimpleDateFormat
import java.util.*
@@ -30,13 +29,14 @@ import kotlin.math.roundToInt
class MessagesPreparator constructor(
private val context: Context,
private val root: View? = null,
private val conversation: VkConversation,
private val message: VkMessage,
private val prevMessage: VkMessage? = null,
private val nextMessage: VkMessage? = null,
private val bubble: BoundedLinearLayout? = null,
private val bubbleStroke: View? = null,
private val text: TextView? = null,
private val avatar: ImageView? = null,
private val title: TextView? = null,
@@ -65,99 +65,43 @@ class MessagesPreparator constructor(
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background)
private val backgroundMiddleOut =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle)
private val backgroundStrokeOut =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke)
private val backgroundMiddleStrokeOut =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke)
// private val backgroundStrokeOut =
// ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke)
// private val backgroundMiddleStrokeOut =
// ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke)
private val rootHighlightedColor =
ContextCompat.getColor(context, R.color.n2_100)
fun prepare() {
val messageUser: VkUser? = if (message.isUser()) {
val messageUser: VkUser? = (if (message.isUser()) {
profiles[message.fromId]
} else null
} else null).also { message.user.value = it }
val messageGroup: VkGroup? = if (message.isGroup()) {
val messageGroup: VkGroup? = (if (message.isGroup()) {
groups[message.fromId]
} else null
} else null).also { message.group.value = it }
if (unread != null) {
unread.isVisible = message.isRead(conversation)
}
prepareRootBackground()
if (bubble != null && time != null) {
bubble.setOnClickListener { time.isVisible = !time.isVisible }
}
prepareTime()
if (attachmentContainer != null) {
if (message.attachments.isNullOrEmpty()) {
attachmentContainer.isVisible = false
attachmentContainer.removeAllViews()
} else {
attachmentContainer.isVisible = true
AttachmentInflater(
context = context,
container = attachmentContainer,
message = message,
groups = groups,
profiles = profiles
).inflate()
}
}
prepareUnreadIndicator()
if (bubble != null) {
val padding =
AndroidUtils.px(if (!message.attachments.isNullOrEmpty()) 4 else 15).roundToInt()
prepareSpacer()
bubble.setPadding(padding)
prepareAttachments()
prepareAttachmentsSpacer()
// TODO: 9/23/2021 use external function
bubble.background =
if (!message.attachments.isNullOrEmpty() && message.attachments!![0] is VkSticker) null
else {
if (message.isOut) {
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormalOut
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleOut
else backgroundNormalOut
} else {
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormalIn
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleIn
else backgroundNormalIn
}
}
}
prepareBubbleBackground()
// TODO: 9/23/2021 use external function
bubbleStroke?.background =
if (bubble?.background == null) null else {
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundStrokeOut
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleStrokeOut
else backgroundStrokeOut
}
prepareText()
if (bubble != null && text != null) {
if (message.text == null) {
text.isVisible = false
bubble.isVisible = !message.attachments.isNullOrEmpty()
bubbleStroke?.isVisible = bubble.isVisible
} else {
text.isVisible = true
bubble.isVisible = true
bubbleStroke?.isVisible = true
text.text = VkUtils.prepareMessageText(message.text)
}
}
if (avatar != null) {
val avatarUrl = when {
message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200
message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200
else -> null
}
avatar.load(avatarUrl) { crossfade(100) }
}
spacer?.isVisible = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message)
prepareAvatar(
messageUser = messageUser,
messageGroup = messageGroup
)
if (message.isPeerChat()) {
@@ -188,19 +132,98 @@ class MessagesPreparator constructor(
title.text = titleString
title.measure(0, 0)
if (bubble != null) {
if (title.isVisible) {
bubble.minimumWidth = title.measuredWidth + 60
} else {
bubble.minimumWidth = 0
}
}
}
}
attachmentSpacer?.isVisible =
!message.attachments.isNullOrEmpty() && text?.isVisible == true
private fun prepareRootBackground() {
if (root != null) {
root.background =
if (message.isSelected) ColorDrawable(rootHighlightedColor)
else null
}
}
private fun prepareTime() {
time?.text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(message.date * 1000L)
}
private fun prepareUnreadIndicator() {
if (unread != null) {
unread.isVisible = message.isRead(conversation)
}
}
private fun prepareSpacer() {
spacer?.isVisible = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message)
}
private fun prepareAttachments() {
if (attachmentContainer != null) {
if (message.attachments.isNullOrEmpty()) {
attachmentContainer.isVisible = false
attachmentContainer.removeAllViews()
} else {
attachmentContainer.isVisible = true
AttachmentInflater(
context = context,
container = attachmentContainer,
message = message,
groups = groups,
profiles = profiles
).inflate()
}
}
}
private fun prepareAttachmentsSpacer() {
attachmentSpacer?.isVisible =
!message.attachments.isNullOrEmpty() && text?.isVisible == true
}
private fun prepareBubbleBackground() {
if (bubble != null) {
// TODO: 9/23/2021 use external function
bubble.background =
if (!message.attachments.isNullOrEmpty() && message.attachments!![0] is VkSticker) null
else {
if (message.isOut) {
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormalOut
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleOut
else backgroundNormalOut
} else {
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormalIn
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleIn
else backgroundNormalIn
}
}
}
}
private fun prepareText() {
if (bubble != null && text != null) {
if (message.text == null) {
text.isVisible = false
bubble.isVisible = !message.attachments.isNullOrEmpty()
} else {
text.isVisible = true
bubble.isVisible = true
text.text = VkUtils.prepareMessageText(message.text)
}
}
}
private fun prepareAvatar(
messageUser: VkUser? = null,
messageGroup: VkGroup? = null
) {
if (avatar != null) {
val avatarUrl = when {
message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200
message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200
else -> null
}
avatar.load(avatarUrl) { crossfade(100) }
}
}
}
@@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:topLeftRadius="30dp"
android:topRightRadius="30dp" />
<solid android:color="@android:color/white" />
</shape>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/white" />
<corners android:radius="100dp" />
</shape>
@@ -6,8 +6,8 @@
<corners
android:bottomLeftRadius="5dp"
android:bottomRightRadius="40dp"
android:bottomRightRadius="30dp"
android:topLeftRadius="30dp"
android:topRightRadius="40dp" />
android:topRightRadius="30dp" />
</shape>
@@ -2,6 +2,10 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="2dp"
android:color="@color/messageOutStrokeColor" />
<solid android:color="@color/messageOutColor" />
<corners
@@ -2,6 +2,10 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="2dp"
android:color="@color/messageOutStrokeColor" />
<solid android:color="@color/messageOutColor" />
<corners
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18.3,5.71c-0.39,-0.39 -1.02,-0.39 -1.41,0L12,10.59 7.11,5.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41L10.59,12 5.7,16.89c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L12,13.41l4.89,4.89c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L13.41,12l4.89,-4.89c0.38,-0.38 0.38,-1.02 0,-1.4z" />
</vector>
@@ -20,18 +20,9 @@
android:elevation="0dp"
app:collapsedTitleTextAppearance="@style/CollapsingToolbarCollapsedTitle"
app:expandedTitleTextAppearance="@style/CollapsingToolbarTitle"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:title="Messages">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:background="?android:windowBackground"
android:elevation="0dp"
app:layout_collapseMode="parallax"
app:menu="@menu/fragment_conversations" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/expandedImage"
android:layout_width="match_parent"
@@ -43,11 +34,13 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="30dp"
android:orientation="horizontal"
android:paddingStart="0dp"
android:paddingEnd="30dp"
app:layout_collapseParallaxMultiplier="0.5">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/search"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginEnd="16dp"
@@ -63,6 +56,42 @@
</androidx.appcompat.widget.LinearLayoutCompat>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:background="?android:windowBackground"
android:elevation="0dp"
app:layout_collapseMode="parallax"
app:menu="@menu/fragment_conversations">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/toolbarAvatarContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="30dp"
android:orientation="horizontal"
app:layout_collapseParallaxMultiplier="0.5">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/toolbarSearch"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginEnd="16dp"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_search"
android:tint="?colorSecondary3Variant" />
<com.meloda.fast.widget.CircleImageView
android:id="@+id/toolbarAvatar"
android:layout_width="30dp"
android:layout_height="30dp"
tools:src="@tools:sample/avatars" />
</androidx.appcompat.widget.LinearLayoutCompat>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.CollapsingToolbarLayout>
@@ -9,6 +9,14 @@
android:layout_height="match_parent"
android:orientation="vertical">
<WebView
android:id="@+id/webView"
android:layout_width="0dp"
android:layout_height="0dp"
android:clickable="false"
android:focusable="false"
android:visibility="gone" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -7,7 +7,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
@@ -49,21 +48,21 @@
android:id="@+id/avatar"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:src="@tools:sample/avatars"
tools:visibility="visible" />
tools:src="@tools:sample/avatars" />
<FrameLayout
android:id="@+id/avatarPlaceholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
android:layout_height="match_parent">
<com.meloda.fast.widget.CircleImageView
android:id="@+id/placeholderBack"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@color/n1_50" />
<com.meloda.fast.widget.CircleImageView
android:id="@+id/placeholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/ic_account_circle_cut"
@@ -90,7 +89,34 @@
android:layout_height="14dp"
android:layout_gravity="center"
android:src="@drawable/ic_online_pc"
app:tint="@color/a3_200" />
app:tint="?colorSecondary2" />
</FrameLayout>
<FrameLayout
android:id="@+id/pin"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="start|top"
android:visibility="gone"
tools:visibility="visible">
<com.meloda.fast.widget.CircleImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center"
android:background="@drawable/ic_back"
android:backgroundTint="@color/n2_500"
android:elevation="0.5dp" />
<com.meloda.fast.widget.CircleImageView
android:id="@+id/pinIcon"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_gravity="center"
android:elevation="1dp"
android:src="@drawable/ic_round_push_pin_24"
app:tint="@color/n2_0" />
</FrameLayout>
@@ -182,6 +208,69 @@
</FrameLayout>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/attachmentPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginBottom="35dp"
android:background="@drawable/ic_chat_attachment_panel_background"
android:backgroundTint="@color/n2_100"
android:minHeight="105dp"
android:orientation="vertical"
android:padding="16dp"
app:layout_anchor="@+id/messagePanel"
app:layout_anchorGravity="center_vertical|top">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/replyMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/replyMessageTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?textColorPrimary"
app:fontFamily="@font/google_sans_regular"
tools:text="Michael Bae" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/dismissReply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_image_button_circle_background"
android:backgroundTint="@color/n1_50"
android:src="@drawable/ic_round_close_20"
android:tint="?colorSecondary3" />
</androidx.appcompat.widget.LinearLayoutCompat>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/replyMessageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?textColorPrimary"
android:textSize="16sp"
app:fontFamily="@font/roboto_regular"
tools:text="Short Message." />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="40dp"
@@ -211,7 +300,8 @@
android:layout_marginHorizontal="20dp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:hint="@string/message_input_hint" />
android:hint="@string/message_input_hint"
android:singleLine="true" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/attach"
@@ -9,12 +9,11 @@
android:orientation="horizontal"
android:padding="4dp">
<com.google.android.material.imageview.ShapeableImageView
<com.meloda.fast.widget.CircleImageView
android:id="@+id/preview"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
<androidx.appcompat.widget.LinearLayoutCompat
+27 -32
View File
@@ -51,31 +51,39 @@
android:layout_height="wrap_content"
android:background="@drawable/ic_message_in_background"
android:backgroundTint="@color/n2_100"
android:minWidth="60dp"
android:orientation="vertical"
android:padding="15dp"
tools:ignore="UselessParent">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:autoLink="all"
android:textColor="@color/n1_800"
tools:text="This" />
<Space
android:id="@+id/attachmentSpacer"
android:layout_width="wrap_content"
android:layout_height="5dp"
android:visibility="gone" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/attachmentContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone" />
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:autoLink="all"
android:padding="15dp"
android:textColor="@color/n1_800"
tools:text="This" />
<Space
android:id="@+id/attachmentSpacer"
android:layout_width="wrap_content"
android:layout_height="5dp"
android:visibility="gone" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/attachmentContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone" />
</androidx.appcompat.widget.LinearLayoutCompat>
</com.meloda.fast.widget.BoundedLinearLayout>
@@ -88,19 +96,6 @@
android:src="@color/a3_200" />
</androidx.appcompat.widget.LinearLayoutCompat>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="12dp"
android:textColor="?textColorSecondaryVariant"
android:visibility="gone"
tools:layout_height="18dp"
tools:text="12:00"
tools:visibility="visible" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
+18 -28
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<androidx.appcompat.widget.LinearLayoutCompat
@@ -30,28 +31,26 @@
android:layout_height="10dp"
android:visibility="gone" />
<FrameLayout
android:id="@+id/bubbleStroke"
<com.meloda.fast.widget.BoundedLinearLayout
android:id="@+id/bubble"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_message_out_background_stroke"
android:padding="1.5dp"
tools:ignore="UselessParent">
android:layout_gravity="center"
android:background="@drawable/ic_message_out_background"
android:clipChildren="true"
android:clipToPadding="true"
android:orientation="vertical">
<com.meloda.fast.widget.BoundedLinearLayout
android:id="@+id/bubble"
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/ic_message_out_background"
android:orientation="vertical"
android:padding="15dp">
android:layout_height="wrap_content">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:padding="15dp"
android:textColor="@color/n1_900"
tools:text="This is test" />
@@ -59,30 +58,21 @@
android:id="@+id/attachmentSpacer"
android:layout_width="wrap_content"
android:layout_height="5dp"
android:layout_below="@+id/text"
android:visibility="gone" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/attachmentContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/attachmentSpacer"
android:orientation="vertical"
android:visibility="gone" />
</com.meloda.fast.widget.BoundedLinearLayout>
</FrameLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="12dp"
android:textColor="?textColorSecondaryVariant"
android:visibility="gone"
tools:layout_height="18dp"
tools:text="12:00"
tools:visibility="visible" />
android:visibility="gone"
app:layout_anchor="@+id/text"
app:layout_anchorGravity="bottom" />
</RelativeLayout>
</com.meloda.fast.widget.BoundedLinearLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
+8
View File
@@ -109,4 +109,12 @@
<string name="post_type_user">User post</string>
<string name="post_type_unknown">Post</string>
<string name="message_attachments_story">Story</string>
<string name="log_out">Log out</string>
<string name="confirm">Confirmation</string>
<string name="log_out_confirm">Are you really want to log out?</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="message_context_action_reply">Reply</string>
<string name="message_context_action_mark_as_important">Mark as important</string>
<string name="message_context_action_unmark_as_important">Unmark as important</string>
</resources>
+4 -18
View File
@@ -1,21 +1,7 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
org.gradle.jvmargs=-Xmx4096M -XX:MaxPermSize=4096m -Dfile.encoding=UTF-8
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=false
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official