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

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("com.android.application")
id("kotlin-android") id("kotlin-android")
id("kotlin-kapt") id("kotlin-kapt")
id("kotlin-parcelize")
id("androidx.navigation.safeargs.kotlin") id("androidx.navigation.safeargs.kotlin")
id("dagger.hilt.android.plugin") id("dagger.hilt.android.plugin")
id("kotlin-parcelize")
} }
android { android {
@@ -38,6 +38,9 @@ android {
getByName("release") { getByName("release") {
isMinifyEnabled = false isMinifyEnabled = false
buildConfigField("String", "vkLogin", login)
buildConfigField("String", "vkPassword", password)
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
@@ -68,7 +71,7 @@ android {
kapt { kapt {
correctErrorTypes = true 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 { javacOptions {
option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true") option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true")
} }
@@ -83,6 +86,8 @@ dependencies {
implementation("androidx.datastore:datastore-preferences:1.0.0") 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("androidx.appcompat:appcompat:1.4.0-alpha03")
implementation("com.google.android.material:material:1.5.0-alpha03") implementation("com.google.android.material:material:1.5.0-alpha03")
implementation("androidx.core:core-ktx:1.7.0-beta01") implementation("androidx.core:core-ktx:1.7.0-beta01")
@@ -6,6 +6,7 @@ import com.meloda.fast.common.AppGlobal
object UserConfig { object UserConfig {
private const val FAST_TOKEN = "fast_token"
private const val TOKEN = "token" private const val TOKEN = "token"
private const val USER_ID = "user_id" private const val USER_ID = "user_id"
@@ -25,8 +26,16 @@ object UserConfig {
AppGlobal.preferences.edit().putString(TOKEN, value).apply() 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() { fun clear() {
accessToken = "" accessToken = ""
fastToken = ""
userId = -1 userId = -1
} }
@@ -7,6 +7,8 @@ object VKConstants {
const val USER_FIELDS = const val USER_FIELDS =
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info" "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 API_VERSION = "5.132"
const val VK_APP_ID = "2274003" const val VK_APP_ID = "2274003"
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH" const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
@@ -42,6 +42,12 @@ object VkUtils {
return forwards return forwards
} }
fun parseReplyMessage(baseReplyMessage: BaseVkMessage?): VkMessage? {
if (baseReplyMessage == null) return null
return baseReplyMessage.asVkMessage()
}
fun parseAttachments(baseAttachments: List<BaseVkAttachmentItem>?): List<VkAttachment>? { fun parseAttachments(baseAttachments: List<BaseVkAttachmentItem>?): List<VkAttachment>? {
if (baseAttachments.isNullOrEmpty()) return null if (baseAttachments.isNullOrEmpty()) return null
@@ -1,9 +1,12 @@
package com.meloda.fast.api.model package com.meloda.fast.api.model
import android.os.Parcelable import androidx.lifecycle.MutableLiveData
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.meloda.fast.api.model.attachments.VkAttachment import com.meloda.fast.api.model.attachments.VkAttachment
import com.meloda.fast.base.adapter.SelectableItem
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Entity(tableName = "messages") @Entity(tableName = "messages")
@@ -24,9 +27,21 @@ data class VkMessage(
val actionMessage: String? = null, val actionMessage: String? = null,
val geoType: String? = null, val geoType: String? = null,
val important: Boolean = false, val important: Boolean = false,
var forwards: List<VkMessage>? = null, var forwards: List<VkMessage>? = null,
var attachments: List<VkAttachment>? = null var attachments: List<VkAttachment>? = null,
) : Parcelable {
// @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 fun isPeerChat() = peerId > 2_000_000_000
@@ -1,8 +1,10 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import androidx.room.Ignore
import com.meloda.fast.api.model.base.attachments.BaseVkPhoto import com.meloda.fast.api.model.base.attachments.BaseVkPhoto
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize @Parcelize
data class VkPhoto( data class VkPhoto(
@@ -17,13 +19,58 @@ data class VkPhoto(
val userId: Int? val userId: Int?
) : VkAttachment() { ) : 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 @IgnoredOnParcel
val className: String = this::class.java.name 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) { 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 size
}
} }
return null return null
@@ -23,7 +23,8 @@ data class BaseVkMessage(
val payload: String, val payload: String,
val geo: Geo?, val geo: Geo?,
val action: Action?, val action: Action?,
val ttl: Int val ttl: Int,
val reply_message: BaseVkMessage?
) : Parcelable { ) : Parcelable {
fun asVkMessage() = VkMessage( fun asVkMessage() = VkMessage(
@@ -44,6 +45,7 @@ data class BaseVkMessage(
).also { ).also {
it.attachments = VkUtils.parseAttachments(attachments) it.attachments = VkUtils.parseAttachments(attachments)
it.forwards = VkUtils.parseForwards(fwd_messages) it.forwards = VkUtils.parseForwards(fwd_messages)
it.replyMessage = VkUtils.parseReplyMessage(reply_message)
} }
@Parcelize @Parcelize
@@ -17,6 +17,7 @@ class AuthInterceptor : Interceptor {
builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8")) 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()) return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build())
} }
@@ -114,7 +114,7 @@ abstract class BaseAdapter<Item, VH : BaseHolder>(
holder.bind(position) holder.bind(position)
} }
protected fun initListeners(itemView: View, position: Int) { protected open fun initListeners(itemView: View, position: Int) {
if (itemView is AdapterView<*>) return if (itemView is AdapterView<*>) return
itemView.setOnClickListener { itemClickListener.invoke(position) } 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, VkUser::class,
VkGroup::class VkGroup::class
], ],
version = 24, version = 25,
exportSchema = false, exportSchema = false,
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
@@ -1,6 +1,8 @@
package com.meloda.fast.extensions package com.meloda.fast.extensions
import android.graphics.* import android.graphics.*
import android.view.View
import androidx.core.view.isVisible
import kotlin.math.min import kotlin.math.min
fun Bitmap.borderedCircularBitmap( fun Bitmap.borderedCircularBitmap(
@@ -70,4 +72,6 @@ fun Bitmap.borderedCircularBitmap(
diameter, // width diameter, // width
diameter // height diameter // height
) )
} }
val View.isNotVisible get() = !isVisible
@@ -1,25 +1,31 @@
package com.meloda.fast.screens.conversations package com.meloda.fast.screens.conversations
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Gravity
import android.view.View import android.view.View
import android.viewbinding.library.fragment.viewBinding import android.viewbinding.library.fragment.viewBinding
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import coil.load import coil.load
import com.google.android.material.appbar.AppBarLayout 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.R
import com.meloda.fast.activity.MainActivity
import com.meloda.fast.api.UserConfig import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.base.BaseViewModelFragment import com.meloda.fast.base.BaseViewModelFragment
import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.AppSettings import com.meloda.fast.common.AppSettings
import com.meloda.fast.common.dataStore import com.meloda.fast.common.dataStore
import com.meloda.fast.databinding.FragmentConversationsBinding import com.meloda.fast.databinding.FragmentConversationsBinding
@@ -29,6 +35,7 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToInt
@AndroidEntryPoint @AndroidEntryPoint
class ConversationsFragment : 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 isPaused = false
private var isExpanded = true private var isExpanded = true
@@ -74,11 +99,7 @@ class ConversationsFragment :
}.collect { } }.collect { }
} }
binding.createChat.setOnClickListener { binding.createChat.setOnClickListener {}
Snackbar.make(it, "Test snackbar", Snackbar.LENGTH_SHORT)
.setAction("Action") {}
.show()
}
UserConfig.vkUser.observe(viewLifecycleOwner) { UserConfig.vkUser.observe(viewLifecycleOwner) {
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } } it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } }
@@ -87,28 +108,49 @@ class ConversationsFragment :
binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset -> binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
if (isPaused) return@OnOffsetChangedListener 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) binding.toolbar.overflowIcon = ContextCompat.getDrawable(requireContext(), R.drawable.test)
viewModel.loadProfileUser()
viewModel.loadConversations()
binding.avatar.setOnClickListener { binding.avatar.setOnClickListener {
lifecycleScope.launchWhenResumed { avatarPopupMenu.show()
}
binding.avatar.setOnLongClickListener {
lifecycleScope.launch {
requireContext().dataStore.edit { settings -> requireContext().dataStore.edit { settings ->
val isMultilineEnabled = settings[AppSettings.keyIsMultilineEnabled] ?: true val isMultilineEnabled = settings[AppSettings.keyIsMultilineEnabled] ?: true
settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled
@@ -117,7 +159,30 @@ class ConversationsFragment :
adapter.notifyItemRangeChanged(0, adapter.itemCount) 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) { override fun onEvent(event: VKEvent) {
@@ -3,13 +3,13 @@ package com.meloda.fast.screens.conversations
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.meloda.fast.api.UserConfig import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants 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.VkConversation
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.request.ConversationsGetRequest import com.meloda.fast.api.model.request.ConversationsGetRequest
import com.meloda.fast.api.model.request.UsersGetRequest 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.BaseViewModel
import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent
@@ -26,13 +26,15 @@ class ConversationsViewModel @Inject constructor(
private val usersDataSource: UsersDataSource private val usersDataSource: UsersDataSource
) : BaseViewModel() { ) : BaseViewModel() {
fun loadConversations() = viewModelScope.launch(Dispatchers.Default) { fun loadConversations(
offset: Int? = null
) = viewModelScope.launch(Dispatchers.Default) {
makeJob({ makeJob({
dataSource.getAllChats( dataSource.getAllChats(
ConversationsGetRequest( ConversationsGetRequest(
count = 30, count = 30,
// offset = 177,
extended = true, extended = true,
offset = offset,
fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}" fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}"
) )
) )
@@ -52,6 +54,7 @@ class ConversationsViewModel @Inject constructor(
sendEvent( sendEvent(
ConversationsLoaded( ConversationsLoaded(
count = response.count, count = response.count,
offset = offset,
unreadCount = response.unreadCount ?: 0, unreadCount = response.unreadCount ?: 0,
conversations = response.items.map { items -> conversations = response.items.map { items ->
items.conversation.asVkConversation( items.conversation.asVkConversation(
@@ -73,9 +76,7 @@ class ConversationsViewModel @Inject constructor(
} }
fun loadProfileUser() = viewModelScope.launch { fun loadProfileUser() = viewModelScope.launch {
makeJob({ makeJob({ usersDataSource.getById(UsersGetRequest(fields = VKConstants.USER_FIELDS)) },
usersDataSource.getById(UsersGetRequest(fields = "online,photo_200"))
},
onAnswer = { onAnswer = {
it.response?.let { r -> it.response?.let { r ->
val users = r.map { u -> u.asVkUser() } val users = r.map { u -> u.asVkUser() }
@@ -89,6 +90,7 @@ class ConversationsViewModel @Inject constructor(
data class ConversationsLoaded( data class ConversationsLoaded(
val count: Int, val count: Int,
val offset: Int?,
val unreadCount: Int?, val unreadCount: Int?,
val conversations: List<VkConversation>, val conversations: List<VkConversation>,
val profiles: HashMap<Int, VkUser>, val profiles: HashMap<Int, VkUser>,
@@ -1,11 +1,17 @@
package com.meloda.fast.screens.login package com.meloda.fast.screens.login
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.viewbinding.library.fragment.viewBinding import android.viewbinding.library.fragment.viewBinding
import android.webkit.CookieManager
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible 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.google.android.material.textfield.TextInputLayout
import com.meloda.fast.BuildConfig import com.meloda.fast.BuildConfig
import com.meloda.fast.R 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.BaseViewModelFragment
import com.meloda.fast.base.viewmodel.* import com.meloda.fast.base.viewmodel.*
import com.meloda.fast.databinding.DialogCaptchaBinding import com.meloda.fast.databinding.DialogCaptchaBinding
@@ -29,7 +37,10 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jsoup.Jsoup
import java.net.URLEncoder
import java.util.* import java.util.*
import java.util.regex.Pattern
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
@AndroidEntryPoint @AndroidEntryPoint
@@ -89,11 +100,91 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
} }
private fun prepareViews() { private fun prepareViews() {
prepareWebView()
prepareEmailEditText() prepareEmailEditText()
preparePasswordEditText() preparePasswordEditText()
prepareAuthButton() 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() { private fun prepareEmailEditText() {
binding.loginInput.addTextChangedListener { binding.loginInput.addTextChangedListener {
if (!binding.loginLayout.error.isNullOrBlank()) binding.loginLayout.error = "" 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 { private fun goToMain(haveAuthorized: Boolean) = lifecycleScope.launch {
if (haveAuthorized) delay(500) if (haveAuthorized) delay(500)
launchWebView()
findNavController().navigate(R.id.toMain) findNavController().navigate(R.id.toMain)
} }
@@ -30,6 +30,7 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
// TODO: 9/29/2021 use recyclerview for viewing attachments
class AttachmentInflater constructor( class AttachmentInflater constructor(
private val context: Context, private val context: Context,
private val container: LinearLayoutCompat, private val container: LinearLayoutCompat,
@@ -94,12 +95,15 @@ class AttachmentInflater constructor(
} }
private fun photo(photo: VkPhoto) { private fun photo(photo: VkPhoto) {
val size = photo.sizeOfType('m') ?: return val size = photo.getSizeOrSmaller('y') ?: return
val newPhoto = ShapeableImageView(context).apply { val newPhoto = ShapeableImageView(context).apply {
layoutParams = LinearLayoutCompat.LayoutParams( layoutParams = LinearLayoutCompat.LayoutParams(
AndroidUtils.px(size.width).roundToInt(), // ViewGroup.LayoutParams.MATCH_PARENT,
AndroidUtils.px(size.height).roundToInt() size.width,
size.height
// AndroidUtils.px(size.width).roundToInt(),
// AndroidUtils.px(size.height).roundToInt()
) )
shapeAppearanceModel = shapeAppearanceModel =
@@ -222,14 +226,7 @@ class AttachmentInflater constructor(
binding.caption.text = link.caption binding.caption.text = link.caption
binding.caption.isVisible = !link.caption.isNullOrBlank() binding.caption.isVisible = !link.caption.isNullOrBlank()
binding.preview.shapeAppearanceModel.toBuilder() link.photo?.getMaxSize()?.let {
.setAllCornerSizes(AndroidUtils.px(20))
.build()
.let {
binding.preview.shapeAppearanceModel = it
}
link.photo?.sizeOfType('m')?.let {
binding.preview.load(it.url) { crossfade(150) } binding.preview.load(it.url) { crossfade(150) }
binding.preview.isVisible = true binding.preview.isVisible = true
return return
@@ -245,8 +242,8 @@ class AttachmentInflater constructor(
with(binding.image) { with(binding.image) {
layoutParams = LinearLayoutCompat.LayoutParams( layoutParams = LinearLayoutCompat.LayoutParams(
AndroidUtils.px(180).roundToInt(), AndroidUtils.px(140).roundToInt(),
AndroidUtils.px(180).roundToInt() AndroidUtils.px(140).roundToInt()
) )
load(url) { crossfade(150) } load(url) { crossfade(150) }
@@ -5,6 +5,7 @@ import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView
import androidx.appcompat.widget.LinearLayoutCompat import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@@ -29,7 +30,11 @@ class MessagesHistoryAdapter constructor(
val conversation: VkConversation, val conversation: VkConversation,
val profiles: HashMap<Int, VkUser> = hashMapOf(), val profiles: HashMap<Int, VkUser> = hashMapOf(),
val groups: HashMap<Int, VkGroup> = 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 { override fun getItemViewType(position: Int): Int {
when { when {
@@ -49,7 +54,7 @@ class MessagesHistoryAdapter constructor(
private fun isPositionHeader(position: Int) = position == 0 private fun isPositionHeader(position: Int) = position == 0
private fun isPositionFooter(position: Int) = position >= actualSize 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) { return when (viewType) {
// magick numbers is great! // magick numbers is great!
HEADER -> Header(createEmptyView(60)) 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 { private fun createEmptyView(size: Int) = View(context).apply {
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
@@ -78,22 +90,22 @@ class MessagesHistoryAdapter constructor(
isFocusable = false isFocusable = false
} }
override fun onBindViewHolder(holder: Holder, position: Int) { override fun onBindViewHolder(holder: BasicHolder, position: Int) {
if (holder is Header || holder is Footer) return if (holder is Header || holder is Footer) return
initListeners(holder.itemView, position) initListeners(holder.itemView, position)
holder.bind(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( inner class IncomingMessage(
private val binding: ItemMessageInBinding private val binding: ItemMessageInBinding
) : Holder(binding.root) { ) : BasicHolder(binding.root) {
override fun bind(position: Int) { override fun bind(position: Int) {
val message = getItem(position) val message = getItem(position)
@@ -103,6 +115,9 @@ class MessagesHistoryAdapter constructor(
MessagesPreparator( MessagesPreparator(
context = context, context = context,
root = binding.root,
conversation = conversation, conversation = conversation,
message = message, message = message,
prevMessage = prevMessage, prevMessage = prevMessage,
@@ -112,7 +127,6 @@ class MessagesHistoryAdapter constructor(
bubble = binding.bubble, bubble = binding.bubble,
text = binding.text, text = binding.text,
spacer = binding.spacer, spacer = binding.spacer,
time = binding.time,
unread = binding.unread, unread = binding.unread,
attachmentContainer = binding.attachmentContainer, attachmentContainer = binding.attachmentContainer,
attachmentSpacer = binding.attachmentSpacer, attachmentSpacer = binding.attachmentSpacer,
@@ -125,11 +139,7 @@ class MessagesHistoryAdapter constructor(
inner class OutgoingMessage( inner class OutgoingMessage(
private val binding: ItemMessageOutBinding private val binding: ItemMessageOutBinding
) : Holder(binding.root) { ) : BasicHolder(binding.root) {
init {
binding.bubbleStroke.setOnClickListener { binding.bubble.performClick() }
}
override fun bind(position: Int) { override fun bind(position: Int) {
val message = getItem(position) val message = getItem(position)
@@ -138,15 +148,14 @@ class MessagesHistoryAdapter constructor(
MessagesPreparator( MessagesPreparator(
context = context, context = context,
root = binding.root,
conversation = conversation, conversation = conversation,
message = message, message = message,
prevMessage = prevMessage, prevMessage = prevMessage,
bubble = binding.bubble, bubble = binding.bubble,
bubbleStroke = binding.bubbleStroke,
text = binding.text, text = binding.text,
spacer = binding.spacer, spacer = binding.spacer,
time = binding.time,
unread = binding.unread, unread = binding.unread,
attachmentContainer = binding.attachmentContainer, attachmentContainer = binding.attachmentContainer,
attachmentSpacer = binding.attachmentSpacer, attachmentSpacer = binding.attachmentSpacer,
@@ -159,7 +168,7 @@ class MessagesHistoryAdapter constructor(
inner class ServiceMessage( inner class ServiceMessage(
private val binding: ItemMessageServiceBinding private val binding: ItemMessageServiceBinding
) : Holder(binding.root) { ) : BasicHolder(binding.root) {
private val youPrefix = context.getString(R.string.you_message_prefix) private val youPrefix = context.getString(R.string.you_message_prefix)
@@ -198,7 +207,7 @@ class MessagesHistoryAdapter constructor(
binding.photo.isVisible = true binding.photo.isVisible = true
val size = attachment.sizeOfType('m') ?: return@let val size = attachment.getSizeOrSmaller('y') ?: return@let
binding.photo.layoutParams = LinearLayoutCompat.LayoutParams( binding.photo.layoutParams = LinearLayoutCompat.LayoutParams(
size.width, size.width,
@@ -213,7 +222,7 @@ class MessagesHistoryAdapter constructor(
} }
} }
private val actualSize get() = values.size val actualSize get() = values.size
override fun getItemCount(): Int { override fun getItemCount(): Int {
if (actualSize == 0) return 2 if (actualSize == 0) return 2
@@ -1,12 +1,15 @@
package com.meloda.fast.screens.messages package com.meloda.fast.screens.messages
import android.graphics.Color import android.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.View import android.view.View
import android.viewbinding.library.fragment.viewBinding 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.isVisible
import androidx.core.view.setPadding
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -16,6 +19,7 @@ import coil.load
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.api.UserConfig 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.VkConversation
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage 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.base.viewmodel.VKEvent
import com.meloda.fast.databinding.FragmentMessagesHistoryBinding import com.meloda.fast.databinding.FragmentMessagesHistoryBinding
import com.meloda.fast.extensions.TextViewExtensions.clear import com.meloda.fast.extensions.TextViewExtensions.clear
import com.meloda.fast.extensions.isNotVisible
import com.meloda.fast.util.AndroidUtils import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.util.TimeUtils import com.meloda.fast.util.TimeUtils
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.math.roundToInt
@AndroidEntryPoint @AndroidEntryPoint
class MessagesHistoryFragment : class MessagesHistoryFragment :
@@ -60,11 +66,14 @@ class MessagesHistoryFragment :
private val adapter: MessagesHistoryAdapter by lazy { private val adapter: MessagesHistoryAdapter by lazy {
MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also { MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also {
it.itemClickListener = this::onItemClick it.onItemClickListener = this::onItemClick
it.itemLongClickListener = this::onItemLongClick it.itemLongClickListener = this::onItemLongClick
} }
} }
private val replyMessage = MutableLiveData<VkMessage?>()
private val isAttachmentPanelVisible = MutableLiveData(false)
private var timestampTimer: Timer? = null private var timestampTimer: Timer? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -98,19 +107,7 @@ class MessagesHistoryFragment :
binding.status.text = status ?: "..." binding.status.text = status ?: "..."
val avatar = when { prepareAvatar()
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
prepareViews() prepareViews()
@@ -166,8 +163,12 @@ class MessagesHistoryFragment :
}) })
binding.message.doAfterTextChanged { binding.message.doAfterTextChanged {
val newValue = if (it.toString().isNotBlank()) Action.SEND val canSend =
else Action.RECORD it.toString().isNotBlank()
val newValue =
if (canSend) Action.SEND
else Action.RECORD
if (action.value != newValue) action.value = newValue if (action.value != newValue) action.value = newValue
} }
@@ -195,12 +196,117 @@ class MessagesHistoryFragment :
else -> return@observe 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() { private fun performAction() {
if (action.value == Action.RECORD) { if (action.value == Action.RECORD) {
return
} else if (action.value == Action.SEND) { } else if (action.value == Action.SEND) {
val messageText = binding.message.text.toString().trim() val messageText = binding.message.text.toString().trim()
if (messageText.isBlank()) return if (messageText.isBlank()) return
@@ -214,18 +320,25 @@ class MessagesHistoryFragment :
peerId = conversation.id, peerId = conversation.id,
fromId = UserConfig.userId, fromId = UserConfig.userId,
date = (date / 1000).toInt(), date = (date / 1000).toInt(),
randomId = 0 randomId = 0,
replyMessage = replyMessage.value
) )
adapter.add(message) adapter.add(message)
adapter.notifyDataSetChanged() adapter.notifyItemInserted(adapter.actualSize - 1)
binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
binding.message.clear() binding.message.clear()
val replyMessage = replyMessage.value
this.replyMessage.value = null
hideAttachmentPanel()
viewModel.sendMessage( viewModel.sendMessage(
peerId = conversation.id, peerId = conversation.id,
message = messageText, message = messageText,
randomId = 0 randomId = 0,
replyTo = replyMessage?.id
) { message = message.copyMessage(id = it) } ) { message = message.copyMessage(id = it) }
} }
} }
@@ -283,17 +396,22 @@ class MessagesHistoryFragment :
private fun markMessagesAsImportant(event: MessagesMarkAsImportant) { private fun markMessagesAsImportant(event: MessagesMarkAsImportant) {
var changed = false var changed = false
val positions = mutableListOf<Int>()
for (i in adapter.values.indices) { for (i in adapter.values.indices) {
val message = adapter.values[i] val message = adapter.values[i]
if (event.messagesIds.contains(message.id)) { if (event.messagesIds.contains(message.id)) {
if (!changed) changed = true if (!changed) changed = true
positions.add(i)
adapter.values[i] = message.copyMessage( adapter.values[i] = message.copyMessage(
important = event.important important = event.important
) )
} }
} }
if (changed) adapter.notifyDataSetChanged() if (changed) positions.forEach { adapter.notifyItemChanged(it) }
} }
private fun refreshMessages(event: MessagesLoaded) { private fun refreshMessages(event: MessagesLoaded) {
@@ -314,21 +432,111 @@ class MessagesHistoryFragment :
else binding.recyclerView.scrollToPosition(adapter.lastPosition) else binding.recyclerView.scrollToPosition(adapter.lastPosition)
} }
private fun onItemClick(position: Int) { private fun onItemClick(position: Int, view: View) {
val message = adapter.values[position] val message = adapter.values[position]
if (message.action != null) return 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()) val dialog = MaterialAlertDialogBuilder(requireContext())
.setItems(params) { _, which -> .setItems(params) { _, which ->
if (which == 0) { when (params[which]) {
viewModel.markAsImportant( important -> viewModel.markAsImportant(
messagesIds = listOf(message.id), messagesIds = listOf(message.id),
important = !message.important 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, peerId: Int,
message: String? = null, message: String? = null,
randomId: Int = 0, randomId: Int = 0,
replyTo: Int? = null,
setId: ((messageId: Int) -> Unit)? = null setId: ((messageId: Int) -> Unit)? = null
) = viewModelScope.launch { ) = viewModelScope.launch {
makeJob( makeJob(
@@ -99,7 +100,8 @@ class MessagesHistoryViewModel @Inject constructor(
MessagesSendRequest( MessagesSendRequest(
peerId = peerId, peerId = peerId,
randomId = randomId, randomId = randomId,
message = message message = message,
replyTo = replyTo
) )
) )
}, },
@@ -1,6 +1,7 @@
package com.meloda.fast.screens.messages package com.meloda.fast.screens.messages
import android.content.Context import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
@@ -10,7 +11,6 @@ import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.setPadding
import coil.load import coil.load
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.api.VkUtils 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.VkUser
import com.meloda.fast.api.model.attachments.VkSticker import com.meloda.fast.api.model.attachments.VkSticker
import com.meloda.fast.common.AppGlobal import com.meloda.fast.common.AppGlobal
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.widget.BoundedLinearLayout import com.meloda.fast.widget.BoundedLinearLayout
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -30,13 +29,14 @@ import kotlin.math.roundToInt
class MessagesPreparator constructor( class MessagesPreparator constructor(
private val context: Context, private val context: Context,
private val root: View? = null,
private val conversation: VkConversation, private val conversation: VkConversation,
private val message: VkMessage, private val message: VkMessage,
private val prevMessage: VkMessage? = null, private val prevMessage: VkMessage? = null,
private val nextMessage: VkMessage? = null, private val nextMessage: VkMessage? = null,
private val bubble: BoundedLinearLayout? = null, private val bubble: BoundedLinearLayout? = null,
private val bubbleStroke: View? = null,
private val text: TextView? = null, private val text: TextView? = null,
private val avatar: ImageView? = null, private val avatar: ImageView? = null,
private val title: TextView? = null, private val title: TextView? = null,
@@ -65,99 +65,43 @@ class MessagesPreparator constructor(
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background) ContextCompat.getDrawable(context, R.drawable.ic_message_out_background)
private val backgroundMiddleOut = private val backgroundMiddleOut =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle) ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle)
private val backgroundStrokeOut = // private val backgroundStrokeOut =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke) // ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke)
private val backgroundMiddleStrokeOut = // private val backgroundMiddleStrokeOut =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke) // ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke)
private val rootHighlightedColor =
ContextCompat.getColor(context, R.color.n2_100)
fun prepare() { fun prepare() {
val messageUser: VkUser? = if (message.isUser()) { val messageUser: VkUser? = (if (message.isUser()) {
profiles[message.fromId] 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] groups[message.fromId]
} else null } else null).also { message.group.value = it }
if (unread != null) { prepareRootBackground()
unread.isVisible = message.isRead(conversation)
}
if (bubble != null && time != null) { prepareTime()
bubble.setOnClickListener { time.isVisible = !time.isVisible }
}
if (attachmentContainer != null) { prepareUnreadIndicator()
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()
}
}
if (bubble != null) { prepareSpacer()
val padding =
AndroidUtils.px(if (!message.attachments.isNullOrEmpty()) 4 else 15).roundToInt()
bubble.setPadding(padding) prepareAttachments()
prepareAttachmentsSpacer()
// TODO: 9/23/2021 use external function prepareBubbleBackground()
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
}
}
}
// TODO: 9/23/2021 use external function prepareText()
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
}
if (bubble != null && text != null) { prepareAvatar(
if (message.text == null) { messageUser = messageUser,
text.isVisible = false messageGroup = messageGroup
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)
if (message.isPeerChat()) { if (message.isPeerChat()) {
@@ -188,19 +132,98 @@ class MessagesPreparator constructor(
title.text = titleString title.text = titleString
title.measure(0, 0) title.measure(0, 0)
if (bubble != null) {
if (title.isVisible) {
bubble.minimumWidth = title.measuredWidth + 60
} else {
bubble.minimumWidth = 0
}
}
} }
}
attachmentSpacer?.isVisible = private fun prepareRootBackground() {
!message.attachments.isNullOrEmpty() && text?.isVisible == true 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) 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" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<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"/> 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> </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 <corners
android:bottomLeftRadius="5dp" android:bottomLeftRadius="5dp"
android:bottomRightRadius="40dp" android:bottomRightRadius="30dp"
android:topLeftRadius="30dp" android:topLeftRadius="30dp"
android:topRightRadius="40dp" /> android:topRightRadius="30dp" />
</shape> </shape>
@@ -2,6 +2,10 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<stroke
android:width="2dp"
android:color="@color/messageOutStrokeColor" />
<solid android:color="@color/messageOutColor" /> <solid android:color="@color/messageOutColor" />
<corners <corners
@@ -2,6 +2,10 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<stroke
android:width="2dp"
android:color="@color/messageOutStrokeColor" />
<solid android:color="@color/messageOutColor" /> <solid android:color="@color/messageOutColor" />
<corners <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" android:elevation="0dp"
app:collapsedTitleTextAppearance="@style/CollapsingToolbarCollapsedTitle" app:collapsedTitleTextAppearance="@style/CollapsingToolbarCollapsedTitle"
app:expandedTitleTextAppearance="@style/CollapsingToolbarTitle" app:expandedTitleTextAppearance="@style/CollapsingToolbarTitle"
app:layout_scrollFlags="scroll|enterAlways|snap" app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:title="Messages"> 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 <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/expandedImage" android:id="@+id/expandedImage"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -43,11 +34,13 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="30dp"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="0dp"
android:paddingEnd="30dp"
app:layout_collapseParallaxMultiplier="0.5"> app:layout_collapseParallaxMultiplier="0.5">
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/search"
android:layout_width="30dp" android:layout_width="30dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
@@ -63,6 +56,42 @@
</androidx.appcompat.widget.LinearLayoutCompat> </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> </com.google.android.material.appbar.CollapsingToolbarLayout>
@@ -9,6 +9,14 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> 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 <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -7,7 +7,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refreshLayout" android:id="@+id/refreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -49,21 +48,21 @@
android:id="@+id/avatar" android:id="@+id/avatar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:src="@tools:sample/avatars" tools:src="@tools:sample/avatars" />
tools:visibility="visible" />
<FrameLayout <FrameLayout
android:id="@+id/avatarPlaceholder" android:id="@+id/avatarPlaceholder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:visibility="gone">
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
android:id="@+id/placeholderBack"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:src="@color/n1_50" /> android:src="@color/n1_50" />
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
android:id="@+id/placeholder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:src="@drawable/ic_account_circle_cut" android:src="@drawable/ic_account_circle_cut"
@@ -90,7 +89,34 @@
android:layout_height="14dp" android:layout_height="14dp"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_online_pc" 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> </FrameLayout>
@@ -182,6 +208,69 @@
</FrameLayout> </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 <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="40dp" android:layout_height="40dp"
@@ -211,7 +300,8 @@
android:layout_marginHorizontal="20dp" android:layout_marginHorizontal="20dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:hint="@string/message_input_hint" /> android:hint="@string/message_input_hint"
android:singleLine="true" />
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/attach" android:id="@+id/attach"
@@ -9,12 +9,11 @@
android:orientation="horizontal" android:orientation="horizontal"
android:padding="4dp"> android:padding="4dp">
<com.google.android.material.imageview.ShapeableImageView <com.meloda.fast.widget.CircleImageView
android:id="@+id/preview" android:id="@+id/preview"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
+27 -32
View File
@@ -51,31 +51,39 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/ic_message_in_background" android:background="@drawable/ic_message_in_background"
android:backgroundTint="@color/n2_100" android:backgroundTint="@color/n2_100"
android:minWidth="60dp"
android:orientation="vertical" android:orientation="vertical"
android:padding="15dp"
tools:ignore="UselessParent"> 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 <androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/attachmentContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical">
android:visibility="gone" />
<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> </com.meloda.fast.widget.BoundedLinearLayout>
@@ -88,19 +96,6 @@
android:src="@color/a3_200" /> android:src="@color/a3_200" />
</androidx.appcompat.widget.LinearLayoutCompat> </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>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout> </layout>
+18 -28
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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"> xmlns:tools="http://schemas.android.com/tools">
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
@@ -30,28 +31,26 @@
android:layout_height="10dp" android:layout_height="10dp"
android:visibility="gone" /> android:visibility="gone" />
<FrameLayout <com.meloda.fast.widget.BoundedLinearLayout
android:id="@+id/bubbleStroke" android:id="@+id/bubble"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/ic_message_out_background_stroke" android:layout_gravity="center"
android:padding="1.5dp" android:background="@drawable/ic_message_out_background"
tools:ignore="UselessParent"> android:clipChildren="true"
android:clipToPadding="true"
android:orientation="vertical">
<com.meloda.fast.widget.BoundedLinearLayout <RelativeLayout
android:id="@+id/bubble"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_gravity="center"
android:background="@drawable/ic_message_out_background"
android:orientation="vertical"
android:padding="15dp">
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/text" android:id="@+id/text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start" android:layout_gravity="center_vertical|start"
android:padding="15dp"
android:textColor="@color/n1_900" android:textColor="@color/n1_900"
tools:text="This is test" /> tools:text="This is test" />
@@ -59,30 +58,21 @@
android:id="@+id/attachmentSpacer" android:id="@+id/attachmentSpacer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="5dp" android:layout_height="5dp"
android:layout_below="@+id/text"
android:visibility="gone" /> android:visibility="gone" />
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/attachmentContainer" android:id="@+id/attachmentContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/attachmentSpacer"
android:orientation="vertical" android:orientation="vertical"
android:visibility="gone" /> android:visibility="gone"
app:layout_anchor="@+id/text"
</com.meloda.fast.widget.BoundedLinearLayout> app:layout_anchorGravity="bottom" />
</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" />
</RelativeLayout>
</com.meloda.fast.widget.BoundedLinearLayout>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout> </layout>
+8
View File
@@ -109,4 +109,12 @@
<string name="post_type_user">User post</string> <string name="post_type_user">User post</string>
<string name="post_type_unknown">Post</string> <string name="post_type_unknown">Post</string>
<string name="message_attachments_story">Story</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> </resources>
+4 -18
View File
@@ -1,21 +1,7 @@
# Project-wide Gradle settings. org.gradle.jvmargs=-Xmx4096M -XX:MaxPermSize=4096m -Dfile.encoding=UTF-8
# IDE (e.g. Android Studio) users: org.gradle.daemon=true
# Gradle settings configured through the IDE *will override* org.gradle.parallel=true
# any settings specified in this file. org.gradle.configureondemand=false
# 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
android.useAndroidX=true android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official