+34
-36
@@ -1,14 +1,17 @@
|
||||
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
val login: String = gradleLocalProperties(rootDir).getProperty("vklogin")
|
||||
val password: String = gradleLocalProperties(rootDir).getProperty("vkpassword")
|
||||
val login: String = gradleLocalProperties(rootDir).getProperty("vkLogin")
|
||||
val password: String = gradleLocalProperties(rootDir).getProperty("vkPassword")
|
||||
|
||||
val sdkPackage: String = gradleLocalProperties(rootDir).getProperty("sdkPackage")
|
||||
val sdkFingerprint: String = gradleLocalProperties(rootDir).getProperty("sdkFingerprint")
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("kotlin-kapt")
|
||||
id("kotlin-parcelize")
|
||||
id("androidx.navigation.safeargs.kotlin")
|
||||
id("dagger.hilt.android.plugin")
|
||||
}
|
||||
|
||||
@@ -34,6 +37,9 @@ android {
|
||||
getByName("debug") {
|
||||
buildConfigField("String", "vkLogin", login)
|
||||
buildConfigField("String", "vkPassword", password)
|
||||
|
||||
buildConfigField("String", "sdkPackage", sdkPackage)
|
||||
buildConfigField("String", "sdkFingerprint", sdkFingerprint)
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = false
|
||||
@@ -41,6 +47,9 @@ android {
|
||||
buildConfigField("String", "vkLogin", login)
|
||||
buildConfigField("String", "vkPassword", password)
|
||||
|
||||
buildConfigField("String", "sdkPackage", sdkPackage)
|
||||
buildConfigField("String", "sdkFingerprint", sdkFingerprint)
|
||||
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
@@ -49,23 +58,13 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
kapt {
|
||||
@@ -78,44 +77,42 @@ kapt {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31")
|
||||
// Cicerone - Navigation
|
||||
implementation("com.github.terrakok:cicerone:7.1")
|
||||
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||
|
||||
implementation("com.github.massoudss:waveformSeekBar:3.1.0")
|
||||
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-beta02")
|
||||
|
||||
implementation("androidx.work:work-runtime-ktx:2.6.0")
|
||||
implementation("androidx.work:work-runtime-ktx:2.7.1")
|
||||
|
||||
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
||||
|
||||
implementation("androidx.paging:paging-runtime-ktx:3.0.1")
|
||||
implementation("androidx.paging:paging-runtime-ktx:3.1.1")
|
||||
|
||||
implementation("androidx.appcompat:appcompat:1.4.0-beta01")
|
||||
implementation("com.google.android.material:material:1.5.0-alpha04")
|
||||
implementation("androidx.core:core-ktx:1.7.0-beta02")
|
||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||
implementation("com.google.android.material:material:1.6.0-beta01")
|
||||
implementation("androidx.core:core-ktx:1.7.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.3.6")
|
||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
|
||||
|
||||
implementation("androidx.room:room-ktx:2.3.0")
|
||||
implementation("androidx.room:room-runtime:2.3.0")
|
||||
kapt("androidx.room:room-compiler:2.3.0")
|
||||
implementation("androidx.room:room-ktx:2.4.2")
|
||||
implementation("androidx.room:room-runtime:2.4.2")
|
||||
kapt("androidx.room:room-compiler:2.4.2")
|
||||
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.3.5")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.3.5")
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.3.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
|
||||
implementation("androidx.lifecycle:lifecycle-common-java8:2.3.1")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.4.1")
|
||||
implementation("androidx.lifecycle:lifecycle-common-java8:2.4.1")
|
||||
|
||||
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.2")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.2")
|
||||
@@ -124,9 +121,8 @@ dependencies {
|
||||
|
||||
implementation("com.google.dagger:hilt-android:2.39.1")
|
||||
kapt("com.google.dagger:hilt-android-compiler:2.39.1")
|
||||
implementation("androidx.hilt:hilt-navigation-fragment:1.0.0")
|
||||
|
||||
implementation("com.github.yogacp:android-viewbinding:1.0.3")
|
||||
implementation("com.github.yogacp:android-viewbinding:1.0.4")
|
||||
|
||||
implementation("io.coil-kt:coil:1.4.0")
|
||||
|
||||
@@ -134,4 +130,6 @@ dependencies {
|
||||
implementation("org.jsoup:jsoup:1.14.3")
|
||||
implementation("ch.acra:acra:4.11.1")
|
||||
|
||||
implementation("com.github.bumptech.glide:glide:4.13.0")
|
||||
kapt("com.github.bumptech.glide:compiler:4.13.0")
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
android:testOnly="false"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:replace="android:allowBackup">
|
||||
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:exported="true"
|
||||
@@ -29,6 +28,11 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.MessagesUpdateService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
||||
@@ -1,17 +1,54 @@
|
||||
package com.meloda.fast.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import com.github.terrakok.cicerone.NavigatorHolder
|
||||
import com.github.terrakok.cicerone.Router
|
||||
import com.github.terrakok.cicerone.androidx.AppNavigator
|
||||
import com.github.terrakok.cicerone.androidx.FragmentScreen
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.base.BaseActivity
|
||||
import com.meloda.fast.common.Screens
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : BaseActivity(R.layout.activity_main) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
private val navigator = object : AppNavigator(this, R.id.root_fragment_container) {
|
||||
override fun setupFragmentTransaction(
|
||||
screen: FragmentScreen,
|
||||
fragmentTransaction: FragmentTransaction,
|
||||
currentFragment: Fragment?,
|
||||
nextFragment: Fragment
|
||||
) {
|
||||
// fragmentTransaction.setCustomAnimations(
|
||||
// R.anim.activity_open_enter, R.anim.activity_close_exit,
|
||||
// R.anim.activity_close_enter, R.anim.activity_open_exit
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var navigatorHolder: NavigatorHolder
|
||||
|
||||
@Inject
|
||||
lateinit var router: Router
|
||||
|
||||
override fun onResumeFragments() {
|
||||
navigatorHolder.setNavigator(navigator)
|
||||
super.onResumeFragments()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
navigatorHolder.removeNavigator()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
router.newRootScreen(Screens.Main())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
enum class ApiEvent(val value: Int) {
|
||||
MESSAGE_SET_FLAGS(2),
|
||||
MESSAGE_CLEAR_FLAGS(3),
|
||||
MESSAGE_NEW(4),
|
||||
MESSAGE_EDIT(5),
|
||||
MESSAGE_READ_INCOMING(6),
|
||||
MESSAGE_READ_OUTGOING(7),
|
||||
FRIEND_ONLINE(8),
|
||||
FRIEND_OFFLINE(9),
|
||||
MESSAGES_DELETED(13),
|
||||
PIN_UNPIN_CONVERSATION(20),
|
||||
PRIVATE_TYPING(61),
|
||||
CHAT_TYPING(62),
|
||||
ONE_MORE_TYPING(63),
|
||||
VOICE_RECORDING(64),
|
||||
PHOTO_UPLOADING(65),
|
||||
VIDEO_UPLOADING(66),
|
||||
FILE_UPLOADING(67),
|
||||
UNREAD_COUNT_UPDATE(80)
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun parse(value: Int) = values().firstOrNull { it.value == value }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
|
||||
sealed class LongPollEvent {
|
||||
|
||||
data class VkMessageNewEvent(
|
||||
val message: VkMessage,
|
||||
val profiles: HashMap<Int, VkUser>,
|
||||
val groups: HashMap<Int, VkGroup>
|
||||
) : LongPollEvent()
|
||||
|
||||
data class VkMessageEditEvent(val message: VkMessage) : LongPollEvent()
|
||||
|
||||
data class VkMessageReadIncomingEvent(val peerId: Int, val messageId: Int) : LongPollEvent()
|
||||
data class VkMessageReadOutgoingEvent(val peerId: Int, val messageId: Int) : LongPollEvent()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.JsonArray
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.api.network.Answer
|
||||
import com.meloda.fast.api.network.messages.MessagesDataSource
|
||||
import com.meloda.fast.api.network.messages.MessagesGetByIdRequest
|
||||
import com.meloda.fast.base.viewmodel.VkEventCallback
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class LongPollUpdatesParser(
|
||||
private val messagesDataSource: MessagesDataSource
|
||||
) : CoroutineScope {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LongPollUpdatesParser"
|
||||
}
|
||||
|
||||
private val job = SupervisorJob()
|
||||
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Log.d(TAG, "error: $throwable")
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Default + job + exceptionHandler
|
||||
|
||||
private val listenersMap: MutableMap<ApiEvent, MutableCollection<VkEventCallback<*>>> =
|
||||
mutableMapOf()
|
||||
|
||||
fun parseNextUpdate(event: JsonArray) {
|
||||
val eventType: ApiEvent? =
|
||||
try {
|
||||
ApiEvent.parse(event[0].asInt)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
if (eventType != null) {
|
||||
println("$TAG: $eventType: $event")
|
||||
} else {
|
||||
println("$TAG: unknown event: $event")
|
||||
}
|
||||
|
||||
when (eventType) {
|
||||
ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event)
|
||||
ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event)
|
||||
ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event)
|
||||
ApiEvent.MESSAGE_EDIT -> parseMessageEdit(eventType, event)
|
||||
ApiEvent.MESSAGE_READ_INCOMING -> parseMessageReadIncoming(eventType, event)
|
||||
ApiEvent.MESSAGE_READ_OUTGOING -> parseMessageReadOutgoing(eventType, event)
|
||||
ApiEvent.FRIEND_ONLINE -> parseFriendOnline(eventType, event)
|
||||
ApiEvent.FRIEND_OFFLINE -> parseFriendOffline(eventType, event)
|
||||
ApiEvent.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event)
|
||||
// ApiEvent.PIN_UNPIN_CONVERSATION -> TODO()
|
||||
// ApiEvent.TYPING -> TODO()
|
||||
// ApiEvent.VOICE_RECORDING -> TODO()
|
||||
// ApiEvent.PHOTO_UPLOADING -> TODO()
|
||||
// ApiEvent.VIDEO_UPLOADING -> TODO()
|
||||
// ApiEvent.FILE_UPLOADING -> TODO()
|
||||
// ApiEvent.UNREAD_COUNT_UPDATE -> TODO()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun parseMessageSetFlags(eventType: ApiEvent, event: JsonArray) {
|
||||
// println("$TAG: $eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseMessageClearFlags(eventType: ApiEvent, event: JsonArray) {
|
||||
// println("$TAG: $eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseMessageNew(eventType: ApiEvent, event: JsonArray) {
|
||||
// println("$TAG: $eventType: $event")
|
||||
|
||||
val messageId = event[1].asInt
|
||||
|
||||
launch {
|
||||
val newMessageEvent: LongPollEvent.VkMessageNewEvent =
|
||||
loadNormalMessage(
|
||||
eventType,
|
||||
messageId
|
||||
)
|
||||
|
||||
listenersMap[ApiEvent.MESSAGE_NEW]?.let {
|
||||
it.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageNewEvent>)
|
||||
.onEvent(newMessageEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageEdit(eventType: ApiEvent, event: JsonArray) {
|
||||
// println("$TAG: $eventType: $event")
|
||||
|
||||
val messageId = event[1].asInt
|
||||
|
||||
launch {
|
||||
val editedMessageEvent: LongPollEvent.VkMessageEditEvent =
|
||||
loadNormalMessage(
|
||||
eventType,
|
||||
messageId
|
||||
)
|
||||
|
||||
listenersMap[ApiEvent.MESSAGE_EDIT]?.let {
|
||||
it.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageEditEvent>)
|
||||
.onEvent(editedMessageEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageReadIncoming(eventType: ApiEvent, event: JsonArray) {
|
||||
val peerId = event[1].asInt
|
||||
val messageId = event[2].asInt
|
||||
|
||||
launch {
|
||||
listenersMap[ApiEvent.MESSAGE_READ_INCOMING]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>)
|
||||
.onEvent(
|
||||
LongPollEvent.VkMessageReadIncomingEvent(
|
||||
peerId = peerId,
|
||||
messageId = messageId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: JsonArray) {
|
||||
val peerId = event[1].asInt
|
||||
val messageId = event[2].asInt
|
||||
|
||||
launch {
|
||||
listenersMap[ApiEvent.MESSAGE_READ_OUTGOING]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>)
|
||||
.onEvent(
|
||||
LongPollEvent.VkMessageReadOutgoingEvent(
|
||||
peerId = peerId,
|
||||
messageId = messageId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseFriendOnline(eventType: ApiEvent, event: JsonArray) {
|
||||
// println("$TAG: $eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseFriendOffline(eventType: ApiEvent, event: JsonArray) {
|
||||
// println("$TAG: $eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseMessagesDeleted(eventType: ApiEvent, event: JsonArray) {
|
||||
// println("$TAG: $eventType: $event")
|
||||
}
|
||||
|
||||
private suspend fun <T : LongPollEvent> loadNormalMessage(eventType: ApiEvent, messageId: Int) =
|
||||
coroutineScope {
|
||||
suspendCoroutine<T> {
|
||||
launch {
|
||||
val normalMessageResponse = messagesDataSource.getById(
|
||||
MessagesGetByIdRequest(
|
||||
messagesIds = listOf(messageId),
|
||||
extended = true,
|
||||
fields = VKConstants.ALL_FIELDS
|
||||
)
|
||||
)
|
||||
|
||||
if (normalMessageResponse !is Answer.Success) {
|
||||
(normalMessageResponse as Answer.Error).throwable.let { throw it }
|
||||
}
|
||||
|
||||
val messagesResponse = normalMessageResponse.data.response ?: return@launch
|
||||
|
||||
val messagesList = messagesResponse.items
|
||||
if (messagesList.isEmpty()) return@launch
|
||||
|
||||
val normalMessage = messagesList[0].asVkMessage()
|
||||
messagesDataSource.store(listOf(normalMessage))
|
||||
|
||||
val profiles = hashMapOf<Int, VkUser>()
|
||||
messagesResponse.profiles?.forEach { baseUser ->
|
||||
baseUser.asVkUser().let { user -> profiles[user.id] = user }
|
||||
}
|
||||
|
||||
val groups = hashMapOf<Int, VkGroup>()
|
||||
messagesResponse.groups?.forEach { baseGroup ->
|
||||
baseGroup.asVkGroup().let { group -> groups[group.id] = group }
|
||||
}
|
||||
|
||||
val resumeValue: LongPollEvent? = when (eventType) {
|
||||
ApiEvent.MESSAGE_NEW ->
|
||||
LongPollEvent.VkMessageNewEvent(
|
||||
normalMessage,
|
||||
profiles,
|
||||
groups
|
||||
)
|
||||
ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(normalMessage)
|
||||
else -> null
|
||||
}
|
||||
|
||||
resumeValue?.let { value -> it.resume(value as T) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun <T : Any> registerListener(eventType: ApiEvent, listener: VkEventCallback<T>) {
|
||||
listenersMap.let { map ->
|
||||
map[eventType] = (map[eventType] ?: mutableListOf()).also {
|
||||
it.add(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) {
|
||||
registerListener(ApiEvent.MESSAGE_READ_INCOMING, listener)
|
||||
}
|
||||
|
||||
fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) {
|
||||
onMessageIncomingRead(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageOutgoingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) {
|
||||
registerListener(ApiEvent.MESSAGE_READ_OUTGOING, listener)
|
||||
}
|
||||
|
||||
fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) {
|
||||
onMessageOutgoingRead(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onNewMessage(listener: VkEventCallback<LongPollEvent.VkMessageNewEvent>) {
|
||||
registerListener(ApiEvent.MESSAGE_NEW, listener)
|
||||
}
|
||||
|
||||
fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) {
|
||||
onNewMessage(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageEdited(listener: VkEventCallback<LongPollEvent.VkMessageEditEvent>) {
|
||||
registerListener(ApiEvent.MESSAGE_EDIT, listener)
|
||||
}
|
||||
|
||||
fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) {
|
||||
onMessageEdited(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun clearListeners() {
|
||||
listenersMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <R : Any> assembleEventCallback(
|
||||
crossinline block: (R) -> Unit
|
||||
): VkEventCallback<R> {
|
||||
return object : VkEventCallback<R> {
|
||||
override fun onEvent(event: R) = block.invoke(event)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.meloda.fast.api
|
||||
|
||||
import com.meloda.fast.api.model.attachments.*
|
||||
|
||||
@Suppress("RemoveExplicitTypeArguments")
|
||||
object VKConstants {
|
||||
|
||||
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
|
||||
@@ -12,10 +13,13 @@ object VKConstants {
|
||||
const val ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS"
|
||||
|
||||
const val API_VERSION = "5.132"
|
||||
const val LP_VERSION = 10
|
||||
|
||||
const val VK_APP_ID = "2274003"
|
||||
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
|
||||
|
||||
const val FAST_GROUP_ID = -119516304
|
||||
const val FAST_APP_ID = "6964679"
|
||||
|
||||
object Auth {
|
||||
const val SCOPE = "notify," +
|
||||
@@ -38,7 +42,7 @@ object VKConstants {
|
||||
}
|
||||
}
|
||||
|
||||
val restrictedToEditAttachments = listOf(
|
||||
val restrictedToEditAttachments = listOf<Class<out VkAttachment>>(
|
||||
VkCall::class.java,
|
||||
VkCurator::class.java,
|
||||
VkEvent::class.java,
|
||||
@@ -46,6 +50,25 @@ object VKConstants {
|
||||
VkGraffiti::class.java,
|
||||
VkGroupCall::class.java,
|
||||
VkStory::class.java,
|
||||
VkVoiceMessage::class.java
|
||||
VkVoiceMessage::class.java,
|
||||
VkWidget::class.java
|
||||
)
|
||||
|
||||
val separatedFromTextAttachments = listOf<Class<out VkAttachment>>(
|
||||
VkPhoto::class.java,
|
||||
VkVideo::class.java,
|
||||
VkSticker::class.java,
|
||||
VkStory::class.java,
|
||||
VkWidget::class.java,
|
||||
VkGroupCall::class.java,
|
||||
VkGroupCall::class.java,
|
||||
VkCurator::class.java,
|
||||
VkEvent::class.java,
|
||||
VkGift::class.java,
|
||||
VkGraffiti::class.java,
|
||||
VkPoll::class.java,
|
||||
VkWall::class.java,
|
||||
VkWallReply::class.java,
|
||||
VkLink::class.java
|
||||
)
|
||||
}
|
||||
@@ -139,86 +139,82 @@ object VkUtils {
|
||||
|
||||
for (baseAttachment in baseAttachments) {
|
||||
when (baseAttachment.getPreparedType()) {
|
||||
BaseVkAttachmentItem.AttachmentType.PHOTO -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Photo -> {
|
||||
val photo = baseAttachment.photo ?: continue
|
||||
attachments += photo.asVkPhoto()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.VIDEO -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Video -> {
|
||||
val video = baseAttachment.video ?: continue
|
||||
attachments += video.asVkVideo()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.AUDIO -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Audio -> {
|
||||
val audio = baseAttachment.audio ?: continue
|
||||
attachments += audio.asVkAudio()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.FILE -> {
|
||||
BaseVkAttachmentItem.AttachmentType.File -> {
|
||||
val file = baseAttachment.file ?: continue
|
||||
attachments += file.asVkFile()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.LINK -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Link -> {
|
||||
val link = baseAttachment.link ?: continue
|
||||
attachments += link.asVkLink()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.MINI_APP -> {
|
||||
BaseVkAttachmentItem.AttachmentType.MiniApp -> {
|
||||
val miniApp = baseAttachment.miniApp ?: continue
|
||||
attachments += VkMiniApp(
|
||||
link = miniApp.app.shareUrl
|
||||
)
|
||||
attachments += miniApp.asVkMiniApp()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.VOICE -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Voice -> {
|
||||
val voiceMessage = baseAttachment.voiceMessage ?: continue
|
||||
attachments += voiceMessage.asVkVoiceMessage()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.STICKER -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Sticker -> {
|
||||
val sticker = baseAttachment.sticker ?: continue
|
||||
attachments += sticker.asVkSticker()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.GIFT -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Gift -> {
|
||||
val gift = baseAttachment.gift ?: continue
|
||||
attachments += gift.asVkGift()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.WALL -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Wall -> {
|
||||
val wall = baseAttachment.wall ?: continue
|
||||
attachments += wall.asVkWall()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.GRAFFITI -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Graffiti -> {
|
||||
val graffiti = baseAttachment.graffiti ?: continue
|
||||
attachments += graffiti.asVkGraffiti()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.POLL -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Poll -> {
|
||||
val poll = baseAttachment.poll ?: continue
|
||||
attachments += VkPoll(
|
||||
id = poll.id
|
||||
)
|
||||
attachments += poll.asVkPoll()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.WALL_REPLY -> {
|
||||
BaseVkAttachmentItem.AttachmentType.WallReply -> {
|
||||
val wallReply = baseAttachment.wallReply ?: continue
|
||||
attachments += VkWallReply(
|
||||
id = wallReply.id
|
||||
)
|
||||
attachments += wallReply.asVkWallReply()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.CALL -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Call -> {
|
||||
val call = baseAttachment.call ?: continue
|
||||
attachments += call.asVkCall()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> {
|
||||
BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> {
|
||||
val groupCall = baseAttachment.groupCall ?: continue
|
||||
attachments += VkGroupCall(
|
||||
initiatorId = groupCall.initiator_id
|
||||
)
|
||||
attachments += groupCall.asVkGroupCall()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.CURATOR -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Curator -> {
|
||||
val curator = baseAttachment.curator ?: continue
|
||||
attachments += curator.asVkCurator()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.EVENT -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Event -> {
|
||||
val event = baseAttachment.event ?: continue
|
||||
attachments += event.asVkEvent()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.STORY -> {
|
||||
BaseVkAttachmentItem.AttachmentType.Story -> {
|
||||
val story = baseAttachment.story ?: continue
|
||||
attachments += story.asVkStory()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.Widget -> {
|
||||
val widget = baseAttachment.widget ?: continue
|
||||
attachments += widget.asVkWidget()
|
||||
}
|
||||
else -> continue
|
||||
}
|
||||
}
|
||||
@@ -580,22 +576,22 @@ object VkUtils {
|
||||
attachmentType: BaseVkAttachmentItem.AttachmentType
|
||||
): Drawable? {
|
||||
val resId = when (attachmentType) {
|
||||
BaseVkAttachmentItem.AttachmentType.PHOTO -> R.drawable.ic_attachment_photo
|
||||
BaseVkAttachmentItem.AttachmentType.VIDEO -> R.drawable.ic_attachment_video
|
||||
BaseVkAttachmentItem.AttachmentType.AUDIO -> R.drawable.ic_attachment_audio
|
||||
BaseVkAttachmentItem.AttachmentType.FILE -> R.drawable.ic_attachment_file
|
||||
BaseVkAttachmentItem.AttachmentType.LINK -> R.drawable.ic_attachment_link
|
||||
BaseVkAttachmentItem.AttachmentType.VOICE -> R.drawable.ic_attachment_voice
|
||||
BaseVkAttachmentItem.AttachmentType.MINI_APP -> R.drawable.ic_attachment_mini_app
|
||||
BaseVkAttachmentItem.AttachmentType.STICKER -> R.drawable.ic_attachment_sticker
|
||||
BaseVkAttachmentItem.AttachmentType.GIFT -> R.drawable.ic_attachment_gift
|
||||
BaseVkAttachmentItem.AttachmentType.WALL -> R.drawable.ic_attachment_wall
|
||||
BaseVkAttachmentItem.AttachmentType.GRAFFITI -> R.drawable.ic_attachment_graffiti
|
||||
BaseVkAttachmentItem.AttachmentType.POLL -> R.drawable.ic_attachment_poll
|
||||
BaseVkAttachmentItem.AttachmentType.WALL_REPLY -> R.drawable.ic_attachment_wall_reply
|
||||
BaseVkAttachmentItem.AttachmentType.CALL -> R.drawable.ic_attachment_call
|
||||
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> R.drawable.ic_attachment_group_call
|
||||
BaseVkAttachmentItem.AttachmentType.STORY -> R.drawable.ic_attachment_story
|
||||
BaseVkAttachmentItem.AttachmentType.Photo -> R.drawable.ic_attachment_photo
|
||||
BaseVkAttachmentItem.AttachmentType.Video -> R.drawable.ic_attachment_video
|
||||
BaseVkAttachmentItem.AttachmentType.Audio -> R.drawable.ic_attachment_audio
|
||||
BaseVkAttachmentItem.AttachmentType.File -> R.drawable.ic_attachment_file
|
||||
BaseVkAttachmentItem.AttachmentType.Link -> R.drawable.ic_attachment_link
|
||||
BaseVkAttachmentItem.AttachmentType.Voice -> R.drawable.ic_attachment_voice
|
||||
BaseVkAttachmentItem.AttachmentType.MiniApp -> R.drawable.ic_attachment_mini_app
|
||||
BaseVkAttachmentItem.AttachmentType.Sticker -> R.drawable.ic_attachment_sticker
|
||||
BaseVkAttachmentItem.AttachmentType.Gift -> R.drawable.ic_attachment_gift
|
||||
BaseVkAttachmentItem.AttachmentType.Wall -> R.drawable.ic_attachment_wall
|
||||
BaseVkAttachmentItem.AttachmentType.Graffiti -> R.drawable.ic_attachment_graffiti
|
||||
BaseVkAttachmentItem.AttachmentType.Poll -> R.drawable.ic_attachment_poll
|
||||
BaseVkAttachmentItem.AttachmentType.WallReply -> R.drawable.ic_attachment_wall_reply
|
||||
BaseVkAttachmentItem.AttachmentType.Call -> R.drawable.ic_attachment_call
|
||||
BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> R.drawable.ic_attachment_group_call
|
||||
BaseVkAttachmentItem.AttachmentType.Story -> R.drawable.ic_attachment_story
|
||||
else -> return null
|
||||
}
|
||||
|
||||
@@ -617,24 +613,25 @@ object VkUtils {
|
||||
|
||||
fun getAttachmentTypeByClass(attachment: VkAttachment): BaseVkAttachmentItem.AttachmentType? {
|
||||
return when (attachment) {
|
||||
is VkPhoto -> BaseVkAttachmentItem.AttachmentType.PHOTO
|
||||
is VkVideo -> BaseVkAttachmentItem.AttachmentType.VIDEO
|
||||
is VkAudio -> BaseVkAttachmentItem.AttachmentType.AUDIO
|
||||
is VkFile -> BaseVkAttachmentItem.AttachmentType.FILE
|
||||
is VkLink -> BaseVkAttachmentItem.AttachmentType.LINK
|
||||
is VkMiniApp -> BaseVkAttachmentItem.AttachmentType.MINI_APP
|
||||
is VkVoiceMessage -> BaseVkAttachmentItem.AttachmentType.VOICE
|
||||
is VkSticker -> BaseVkAttachmentItem.AttachmentType.STICKER
|
||||
is VkGift -> BaseVkAttachmentItem.AttachmentType.GIFT
|
||||
is VkWall -> BaseVkAttachmentItem.AttachmentType.WALL
|
||||
is VkGraffiti -> BaseVkAttachmentItem.AttachmentType.GRAFFITI
|
||||
is VkPoll -> BaseVkAttachmentItem.AttachmentType.POLL
|
||||
is VkWallReply -> BaseVkAttachmentItem.AttachmentType.WALL_REPLY
|
||||
is VkCall -> BaseVkAttachmentItem.AttachmentType.CALL
|
||||
is VkGroupCall -> BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS
|
||||
is VkEvent -> BaseVkAttachmentItem.AttachmentType.EVENT
|
||||
is VkCurator -> BaseVkAttachmentItem.AttachmentType.CURATOR
|
||||
is VkStory -> BaseVkAttachmentItem.AttachmentType.STORY
|
||||
is VkPhoto -> BaseVkAttachmentItem.AttachmentType.Photo
|
||||
is VkVideo -> BaseVkAttachmentItem.AttachmentType.Video
|
||||
is VkAudio -> BaseVkAttachmentItem.AttachmentType.Audio
|
||||
is VkFile -> BaseVkAttachmentItem.AttachmentType.File
|
||||
is VkLink -> BaseVkAttachmentItem.AttachmentType.Link
|
||||
is VkMiniApp -> BaseVkAttachmentItem.AttachmentType.MiniApp
|
||||
is VkVoiceMessage -> BaseVkAttachmentItem.AttachmentType.Voice
|
||||
is VkSticker -> BaseVkAttachmentItem.AttachmentType.Sticker
|
||||
is VkGift -> BaseVkAttachmentItem.AttachmentType.Gift
|
||||
is VkWall -> BaseVkAttachmentItem.AttachmentType.Wall
|
||||
is VkGraffiti -> BaseVkAttachmentItem.AttachmentType.Graffiti
|
||||
is VkPoll -> BaseVkAttachmentItem.AttachmentType.Poll
|
||||
is VkWallReply -> BaseVkAttachmentItem.AttachmentType.WallReply
|
||||
is VkCall -> BaseVkAttachmentItem.AttachmentType.Call
|
||||
is VkGroupCall -> BaseVkAttachmentItem.AttachmentType.GroupCallInProgress
|
||||
is VkEvent -> BaseVkAttachmentItem.AttachmentType.Event
|
||||
is VkCurator -> BaseVkAttachmentItem.AttachmentType.Curator
|
||||
is VkStory -> BaseVkAttachmentItem.AttachmentType.Story
|
||||
is VkWidget -> BaseVkAttachmentItem.AttachmentType.Widget
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -645,42 +642,44 @@ object VkUtils {
|
||||
size: Int = 1
|
||||
): String {
|
||||
return when (attachmentType) {
|
||||
BaseVkAttachmentItem.AttachmentType.PHOTO ->
|
||||
BaseVkAttachmentItem.AttachmentType.Photo ->
|
||||
context.resources.getQuantityString(R.plurals.attachment_photos, size, size)
|
||||
BaseVkAttachmentItem.AttachmentType.VIDEO ->
|
||||
BaseVkAttachmentItem.AttachmentType.Video ->
|
||||
context.resources.getQuantityString(R.plurals.attachment_videos, size, size)
|
||||
BaseVkAttachmentItem.AttachmentType.AUDIO ->
|
||||
BaseVkAttachmentItem.AttachmentType.Audio ->
|
||||
context.resources.getQuantityString(R.plurals.attachment_audios, size, size)
|
||||
BaseVkAttachmentItem.AttachmentType.FILE ->
|
||||
BaseVkAttachmentItem.AttachmentType.File ->
|
||||
context.resources.getQuantityString(R.plurals.attachment_files, size, size)
|
||||
BaseVkAttachmentItem.AttachmentType.LINK ->
|
||||
BaseVkAttachmentItem.AttachmentType.Link ->
|
||||
context.resources.getString(R.string.message_attachments_link)
|
||||
BaseVkAttachmentItem.AttachmentType.VOICE ->
|
||||
BaseVkAttachmentItem.AttachmentType.Voice ->
|
||||
context.resources.getString(R.string.message_attachments_voice)
|
||||
BaseVkAttachmentItem.AttachmentType.MINI_APP ->
|
||||
BaseVkAttachmentItem.AttachmentType.MiniApp ->
|
||||
context.resources.getString(R.string.message_attachments_mini_app)
|
||||
BaseVkAttachmentItem.AttachmentType.STICKER ->
|
||||
BaseVkAttachmentItem.AttachmentType.Sticker ->
|
||||
context.resources.getString(R.string.message_attachments_sticker)
|
||||
BaseVkAttachmentItem.AttachmentType.GIFT ->
|
||||
BaseVkAttachmentItem.AttachmentType.Gift ->
|
||||
context.resources.getString(R.string.message_attachments_gift)
|
||||
BaseVkAttachmentItem.AttachmentType.WALL ->
|
||||
BaseVkAttachmentItem.AttachmentType.Wall ->
|
||||
context.resources.getString(R.string.message_attachments_wall)
|
||||
BaseVkAttachmentItem.AttachmentType.GRAFFITI ->
|
||||
BaseVkAttachmentItem.AttachmentType.Graffiti ->
|
||||
context.resources.getString(R.string.message_attachments_graffiti)
|
||||
BaseVkAttachmentItem.AttachmentType.POLL ->
|
||||
BaseVkAttachmentItem.AttachmentType.Poll ->
|
||||
context.resources.getString(R.string.message_attachments_poll)
|
||||
BaseVkAttachmentItem.AttachmentType.WALL_REPLY ->
|
||||
BaseVkAttachmentItem.AttachmentType.WallReply ->
|
||||
context.resources.getString(R.string.message_attachments_wall_reply)
|
||||
BaseVkAttachmentItem.AttachmentType.CALL ->
|
||||
BaseVkAttachmentItem.AttachmentType.Call ->
|
||||
context.resources.getString(R.string.message_attachments_call)
|
||||
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS ->
|
||||
BaseVkAttachmentItem.AttachmentType.GroupCallInProgress ->
|
||||
context.resources.getString(R.string.message_attachments_call_in_progress)
|
||||
BaseVkAttachmentItem.AttachmentType.EVENT ->
|
||||
BaseVkAttachmentItem.AttachmentType.Event ->
|
||||
context.resources.getString(R.string.message_attachments_event)
|
||||
BaseVkAttachmentItem.AttachmentType.CURATOR ->
|
||||
BaseVkAttachmentItem.AttachmentType.Curator ->
|
||||
context.resources.getString(R.string.message_attachments_curator)
|
||||
BaseVkAttachmentItem.AttachmentType.STORY ->
|
||||
BaseVkAttachmentItem.AttachmentType.Story ->
|
||||
context.resources.getString(R.string.message_attachments_story)
|
||||
BaseVkAttachmentItem.AttachmentType.Widget ->
|
||||
context.resources.getString(R.string.message_attachments_widget)
|
||||
else -> attachmentType.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import com.meloda.fast.model.SelectableItem
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@@ -35,7 +35,7 @@ data class VkConversation(
|
||||
|
||||
@Embedded(prefix = "lastMessage_")
|
||||
var lastMessage: VkMessage? = null,
|
||||
) : Parcelable {
|
||||
) : SelectableItem(id) {
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
|
||||
@@ -7,7 +7,7 @@ import androidx.room.PrimaryKey
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||
import com.meloda.fast.base.adapter.SelectableItem
|
||||
import com.meloda.fast.model.SelectableItem
|
||||
import com.meloda.fast.util.TimeUtils
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -33,10 +33,8 @@ data class VkMessage(
|
||||
|
||||
var forwards: List<VkMessage>? = null,
|
||||
var attachments: List<VkAttachment>? = null,
|
||||
|
||||
// @Embedded(prefix = "replyMessage_")
|
||||
var replyMessage: VkMessage? = null
|
||||
) : SelectableItem() {
|
||||
) : SelectableItem(id) {
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
@@ -53,8 +51,8 @@ data class VkMessage(
|
||||
fun isGroup() = fromId < 0
|
||||
|
||||
fun isRead(conversation: VkConversation) =
|
||||
if (isOut) conversation.outRead < id
|
||||
else conversation.inRead < id
|
||||
if (isOut) conversation.outRead - id >= 0
|
||||
else conversation.inRead - id >= 0
|
||||
|
||||
fun getPreparedAction(): Action? {
|
||||
if (action == null) return null
|
||||
|
||||
@@ -20,21 +20,30 @@ data class VkPhoto(
|
||||
val userId: Int?
|
||||
) : VkAttachment() {
|
||||
|
||||
companion object {
|
||||
const val SIZE_TYPE_75 = 's'
|
||||
const val SIZE_TYPE_130 = 'm'
|
||||
const val SIZE_TYPE_604 = 'x'
|
||||
const val SIZE_TYPE_807 = 'y'
|
||||
const val SIZE_TYPE_1080_1024 = 'z'
|
||||
const val SIZE_TYPE_2560_2048 = 'w'
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
private val sizesChars = Stack<Char>()
|
||||
|
||||
init {
|
||||
sizesChars.push('s')
|
||||
sizesChars.push('m')
|
||||
sizesChars.push('x')
|
||||
sizesChars.push(SIZE_TYPE_75)
|
||||
sizesChars.push(SIZE_TYPE_130)
|
||||
sizesChars.push(SIZE_TYPE_604)
|
||||
sizesChars.push('o')
|
||||
sizesChars.push('p')
|
||||
sizesChars.push('q')
|
||||
sizesChars.push('r')
|
||||
sizesChars.push('y')
|
||||
sizesChars.push('z')
|
||||
sizesChars.push('w')
|
||||
sizesChars.push(SIZE_TYPE_807)
|
||||
sizesChars.push(SIZE_TYPE_1080_1024)
|
||||
sizesChars.push(SIZE_TYPE_2560_2048)
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
@@ -61,7 +70,7 @@ data class VkPhoto(
|
||||
}
|
||||
|
||||
fun getSizeOrSmaller(type: Char): BaseVkPhoto.Size? {
|
||||
val photoStack = sizesChars.clone() as Stack<Char>
|
||||
val photoStack = sizesChars.clone() as Stack<*>
|
||||
|
||||
val sizeIndex = photoStack.search(type)
|
||||
|
||||
@@ -72,7 +81,7 @@ data class VkPhoto(
|
||||
}
|
||||
|
||||
for (i in 0 until photoStack.size) {
|
||||
val size = getSizeOrNull(photoStack.peek())
|
||||
val size = getSizeOrNull(photoStack.peek() as Char)
|
||||
|
||||
if (size == null) {
|
||||
photoStack.pop()
|
||||
|
||||
@@ -7,5 +7,11 @@ data class VkStory(
|
||||
val id: Int,
|
||||
val ownerId: Int,
|
||||
val date: Int,
|
||||
val photo: VkPhoto
|
||||
) : VkAttachment()
|
||||
val photo: VkPhoto?
|
||||
) : VkAttachment() {
|
||||
|
||||
fun isFromUser() = ownerId > 0
|
||||
|
||||
fun isFromGroup() = ownerId < 0
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkVideo
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
@@ -9,7 +10,7 @@ import kotlinx.parcelize.Parcelize
|
||||
data class VkVideo(
|
||||
val id: Int,
|
||||
val ownerId: Int,
|
||||
val images: List<BaseVkVideo.Image>,
|
||||
val images: List<VideoImage>,
|
||||
val firstFrames: List<BaseVkVideo.FirstFrame>?,
|
||||
val accessKey: String?
|
||||
) : VkAttachment() {
|
||||
@@ -17,10 +18,57 @@ data class VkVideo(
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
fun imageForWidth(width: Int): BaseVkVideo.Image? {
|
||||
fun imageForWidth(width: Int): VideoImage? {
|
||||
return images.find { it.width == width }
|
||||
}
|
||||
|
||||
fun imageForWidthAtLeast(width: Int): VideoImage? {
|
||||
var certainImages = images.sortedByDescending { it.width }
|
||||
var containsVertical = false
|
||||
for (image in images) {
|
||||
if (image.shapeKind == ShapeKind.Vertical) {
|
||||
containsVertical = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (containsVertical) {
|
||||
certainImages = certainImages.filter { it.shapeKind == ShapeKind.Vertical }
|
||||
}
|
||||
|
||||
certainImages = certainImages.filter { it.width >= width }
|
||||
|
||||
return certainImages.firstOrNull()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class VideoImage(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val withPadding: Boolean
|
||||
) : Parcelable {
|
||||
|
||||
@IgnoredOnParcel
|
||||
var shapeKind: ShapeKind
|
||||
|
||||
init {
|
||||
val ratio = width.toFloat() / height.toFloat()
|
||||
|
||||
shapeKind = when {
|
||||
ratio > 1 -> ShapeKind.Horizontal
|
||||
ratio < 1 -> ShapeKind.Vertical
|
||||
else -> ShapeKind.Square
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ShapeKind {
|
||||
object Vertical : ShapeKind()
|
||||
object Horizontal : ShapeKind()
|
||||
object Square : ShapeKind()
|
||||
}
|
||||
|
||||
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
||||
attachmentClass = this::class.java,
|
||||
id = id,
|
||||
|
||||
@@ -12,8 +12,8 @@ data class VkVoiceMessage(
|
||||
val linkOgg: String,
|
||||
val linkMp3: String,
|
||||
val accessKey: String,
|
||||
val transcriptState: String,
|
||||
val transcript: String
|
||||
val transcriptState: String?,
|
||||
val transcript: String?
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkWidget(
|
||||
val id: Int
|
||||
) : VkAttachment()
|
||||
@@ -8,17 +8,17 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkMessage(
|
||||
val id: Int,
|
||||
val peer_id: Int,
|
||||
val date: Int,
|
||||
val from_id: Int,
|
||||
val id: Int,
|
||||
val out: Int,
|
||||
val peer_id: Int,
|
||||
val text: String,
|
||||
val conversation_message_id: Int,
|
||||
val fwd_messages: List<BaseVkMessage>? = listOf(),
|
||||
val fwd_messages: List<BaseVkMessage>? = emptyList(),
|
||||
val important: Boolean,
|
||||
val random_id: Int,
|
||||
val attachments: List<BaseVkAttachmentItem> = listOf(),
|
||||
val attachments: List<BaseVkAttachmentItem> = emptyList(),
|
||||
val is_hidden: Boolean,
|
||||
val payload: String,
|
||||
val geo: Geo?,
|
||||
@@ -29,7 +29,7 @@ data class BaseVkMessage(
|
||||
|
||||
fun asVkMessage() = VkMessage(
|
||||
id = id,
|
||||
text = if (text.isBlank()) null else text,
|
||||
text = text.ifBlank { null },
|
||||
isOut = out == 1,
|
||||
peerId = peer_id,
|
||||
fromId = from_id,
|
||||
|
||||
+25
-23
@@ -30,38 +30,40 @@ data class BaseVkAttachmentItem(
|
||||
val groupCall: BaseVkGroupCall?,
|
||||
val curator: BaseVkCurator?,
|
||||
val event: BaseVkEvent?,
|
||||
val story: BaseVkStory?
|
||||
val story: BaseVkStory?,
|
||||
val widget: BaseVkWidget?
|
||||
) : Parcelable {
|
||||
|
||||
fun getPreparedType() = AttachmentType.parse(type)
|
||||
|
||||
enum class AttachmentType(var value: String) {
|
||||
UNKNOWN("unknown"),
|
||||
PHOTO("photo"),
|
||||
VIDEO("video"),
|
||||
AUDIO("audio"),
|
||||
FILE("doc"),
|
||||
LINK("link"),
|
||||
VOICE("audio_message"),
|
||||
MINI_APP("mini_app"),
|
||||
STICKER("sticker"),
|
||||
GIFT("gift"),
|
||||
WALL("wall"),
|
||||
GRAFFITI("graffiti"),
|
||||
POLL("poll"),
|
||||
WALL_REPLY("wall_reply"),
|
||||
CALL("call"),
|
||||
GROUP_CALL_IN_PROGRESS("group_call_in_progress"),
|
||||
CURATOR("curator"),
|
||||
EVENT("event"),
|
||||
STORY("story")
|
||||
Unknown("unknown"),
|
||||
Photo("photo"),
|
||||
Video("video"),
|
||||
Audio("audio"),
|
||||
File("doc"),
|
||||
Link("link"),
|
||||
Voice("audio_message"),
|
||||
MiniApp("mini_app"),
|
||||
Sticker("sticker"),
|
||||
Gift("gift"),
|
||||
Wall("wall"),
|
||||
Graffiti("graffiti"),
|
||||
Poll("poll"),
|
||||
WallReply("wall_reply"),
|
||||
Call("call"),
|
||||
GroupCallInProgress("group_call_in_progress"),
|
||||
Curator("curator"),
|
||||
Event("event"),
|
||||
Story("story"),
|
||||
Widget("widget")
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun parse(value: String): AttachmentType? {
|
||||
val parsedValue = values().firstOrNull { it.value == value } ?: UNKNOWN
|
||||
fun parse(value: String): AttachmentType {
|
||||
val parsedValue = values().firstOrNull { it.value == value } ?: Unknown
|
||||
|
||||
if (parsedValue == UNKNOWN) {
|
||||
if (parsedValue == Unknown) {
|
||||
Log.e("AttachmentType", "Unknown attachment type: $value")
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,11 @@ data class BaseVkEvent(
|
||||
val is_favorite: Boolean,
|
||||
val text: String,
|
||||
val address: String,
|
||||
val friends: List<Int> = listOf(),
|
||||
val friends: List<Int> = emptyList(),
|
||||
val member_status: Int,
|
||||
val time: Int
|
||||
) : BaseVkAttachment() {
|
||||
|
||||
fun asVkEvent() = VkEvent(
|
||||
id = id
|
||||
)
|
||||
fun asVkEvent() = VkEvent(id = id)
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkGroupCall
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -16,4 +17,6 @@ data class BaseVkGroupCall(
|
||||
val count: Int
|
||||
) : Parcelable
|
||||
|
||||
fun asVkGroupCall() = VkGroupCall(initiatorId = initiator_id)
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.meloda.fast.api.model.attachments.VkMiniApp
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -63,4 +64,6 @@ data class BaseVkMiniApp(
|
||||
val url: String
|
||||
) : Parcelable
|
||||
|
||||
fun asVkMiniApp() = VkMiniApp(link = app.shareUrl)
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkPoll
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -55,7 +56,8 @@ data class BaseVkPoll(
|
||||
val color: String,
|
||||
val position: Double
|
||||
) : Parcelable
|
||||
|
||||
}
|
||||
|
||||
fun asVkPoll() = VkPoll(id = id)
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ data class BaseVkStory(
|
||||
val date: Int,
|
||||
val expires_at: Int,
|
||||
val is_ads: Boolean,
|
||||
val photo: BaseVkPhoto,
|
||||
val photo: BaseVkPhoto?,
|
||||
val replies: Replies,
|
||||
val is_one_time: Boolean,
|
||||
val track_code: String,
|
||||
@@ -40,7 +40,7 @@ data class BaseVkStory(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
date = date,
|
||||
photo = photo.asVkPhoto()
|
||||
photo = photo?.asVkPhoto()
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
|
||||
@@ -41,18 +41,26 @@ data class BaseVkVideo(
|
||||
fun asVkVideo() = VkVideo(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
images = image,
|
||||
images = image.map { it.asVideoImage() },
|
||||
firstFrames = first_frame,
|
||||
accessKey = access_key
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class Image(
|
||||
val height: Int,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val with_padding: Int
|
||||
) : Parcelable
|
||||
val with_padding: Int?
|
||||
) : Parcelable {
|
||||
|
||||
fun asVideoImage() = VkVideo.VideoImage(
|
||||
width = width,
|
||||
height = height,
|
||||
url = url,
|
||||
withPadding = with_padding == 1
|
||||
)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class FirstFrame(
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ data class BaseVkVoiceMessage(
|
||||
val link_ogg: String,
|
||||
val link_mp3: String,
|
||||
val access_key: String,
|
||||
val transcript_state: String,
|
||||
val transcript: String
|
||||
val transcript_state: String?,
|
||||
val transcript: String?
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkVoiceMessage() = VkVoiceMessage(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkWallReply
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -17,7 +18,6 @@ data class BaseVkWallReply(
|
||||
val reply_to_comment: Int?
|
||||
) : Parcelable {
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class Likes(
|
||||
val count: Int,
|
||||
@@ -26,4 +26,6 @@ data class BaseVkWallReply(
|
||||
val can_publish: Int
|
||||
) : Parcelable
|
||||
|
||||
fun asVkWallReply() = VkWallReply(id = id)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import com.meloda.fast.api.model.attachments.VkWidget
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class BaseVkWidget(val id: Int) : BaseVkAttachment() {
|
||||
|
||||
fun asVkWidget() = VkWidget(id)
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.meloda.fast.api.network
|
||||
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.network.account.AccountUrls
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.net.URLEncoder
|
||||
@@ -12,10 +13,12 @@ class AuthInterceptor : Interceptor {
|
||||
val builder = chain.request().url.newBuilder()
|
||||
.addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8"))
|
||||
|
||||
UserConfig.accessToken.let {
|
||||
if (it.isNotBlank())
|
||||
builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8"))
|
||||
}
|
||||
|
||||
if (!builder.build().toUrl().toString().contains(AccountUrls.SetOnline))
|
||||
UserConfig.accessToken.let {
|
||||
if (it.isNotBlank())
|
||||
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())
|
||||
|
||||
@@ -5,6 +5,36 @@ object VkUrls {
|
||||
const val OAUTH = "https://oauth.vk.com"
|
||||
const val API = "https://api.vk.com/method"
|
||||
|
||||
object Auth {
|
||||
const val DirectAuth = "$OAUTH/token"
|
||||
const val SendSms = "$API/auth.validatePhone"
|
||||
}
|
||||
|
||||
object Conversations {
|
||||
const val Get = "$API/messages.getConversations"
|
||||
const val Delete = "$API/messages.deleteConversation"
|
||||
const val Pin = "$API/messages.pinConversation"
|
||||
const val Unpin = "$API/messages.unpinConversation"
|
||||
const val ReorderPinned = "$API/messages.reorderPinnedConversations"
|
||||
}
|
||||
|
||||
object Users {
|
||||
const val GetById = "$API/users.get"
|
||||
}
|
||||
|
||||
object Messages {
|
||||
const val GetHistory = "$API/messages.getHistory"
|
||||
const val Send = "$API/messages.send"
|
||||
const val MarkAsImportant = "$API/messages.markAsImportant"
|
||||
const val GetLongPollServer = "$API/messages.getLongPollServer"
|
||||
const val GetLongPollHistory = "$API/messages.getLongPollHistory"
|
||||
const val Pin = "$API/messages.pin"
|
||||
const val Unpin = "$API/messages.unpin"
|
||||
const val Delete = "$API/messages.delete"
|
||||
const val Edit = "$API/messages.edit"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.meloda.fast.api.network.account
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class AccountDataSource @Inject constructor(
|
||||
private val repo: AccountRepo
|
||||
) {
|
||||
|
||||
|
||||
suspend fun setOnline(params: AccountSetOnlineRequest) = repo.setOnline(params.map)
|
||||
|
||||
suspend fun setOffline(params: AccountSetOfflineRequest) = repo.setOffline(params.map)
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.meloda.fast.api.network.account
|
||||
|
||||
import com.meloda.fast.api.base.ApiResponse
|
||||
import com.meloda.fast.api.network.Answer
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.QueryMap
|
||||
|
||||
interface AccountRepo {
|
||||
|
||||
@GET(AccountUrls.SetOnline)
|
||||
suspend fun setOnline(@QueryMap params: Map<String, String>): Answer<ApiResponse<Any>>
|
||||
|
||||
@POST(AccountUrls.SetOffline)
|
||||
suspend fun setOffline(@QueryMap params: Map<String, String>): Answer<ApiResponse<Any>>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.meloda.fast.api.network.account
|
||||
|
||||
import com.meloda.fast.api.ApiExtensions.intString
|
||||
|
||||
data class AccountSetOnlineRequest(
|
||||
val voip: Boolean,
|
||||
val accessToken: String
|
||||
) {
|
||||
|
||||
val map
|
||||
get() = mutableMapOf(
|
||||
"voip" to voip.intString,
|
||||
"access_token" to accessToken
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
data class AccountSetOfflineRequest(val accessToken: String) {
|
||||
val map get() = mutableMapOf("access_token" to accessToken)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.meloda.fast.api.network.account
|
||||
|
||||
import com.meloda.fast.api.network.VkUrls
|
||||
|
||||
object AccountUrls {
|
||||
|
||||
const val SetOnline = "${VkUrls.API}/account.setOnline"
|
||||
const val SetOffline = "${VkUrls.API}/account.setOffline"
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ class AuthDataSource @Inject constructor(
|
||||
private val repo: AuthRepo
|
||||
) {
|
||||
|
||||
suspend fun auth(params: RequestAuthDirect) = repo.auth(params.map)
|
||||
suspend fun auth(params: AuthDirectRequest) = repo.auth(params.map)
|
||||
|
||||
suspend fun sendSms(validationSid: String) = repo.sendSms(validationSid)
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import retrofit2.http.QueryMap
|
||||
interface AuthRepo {
|
||||
|
||||
@GET(AuthUrls.DirectAuth)
|
||||
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<ResponseAuthDirect>
|
||||
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<AuthDirectResponse>
|
||||
|
||||
@GET(AuthUrls.SendSms)
|
||||
suspend fun sendSms(@Query("sid") validationSid: String): Answer<ResponseSendSms>
|
||||
suspend fun sendSms(@Query("sid") validationSid: String): Answer<SendSmsResponse>
|
||||
|
||||
}
|
||||
@@ -1,23 +1,25 @@
|
||||
package com.meloda.fast.api.network.auth
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.meloda.fast.BuildConfig
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class RequestAuthDirect(
|
||||
@SerializedName("grant_type") val grantType: String,
|
||||
@SerializedName("client_id") val clientId: String,
|
||||
@SerializedName("client_secret") val clientSecret: String,
|
||||
@SerializedName("username") val username: String,
|
||||
@SerializedName("password") val password: String,
|
||||
@SerializedName("scope") val scope: String,
|
||||
@SerializedName("2fa_supported") val twoFaSupported: Boolean = true,
|
||||
@SerializedName("force_sms") val twoFaForceSms: Boolean = false,
|
||||
@SerializedName("code") val twoFaCode: String? = null,
|
||||
@SerializedName("captcha_sid") val captchaSid: String? = null,
|
||||
@SerializedName("captcha_key") val captchaKey: String? = null,
|
||||
data class AuthDirectRequest(
|
||||
val grantType: String,
|
||||
val clientId: String,
|
||||
val clientSecret: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val scope: String,
|
||||
val twoFaSupported: Boolean = true,
|
||||
val twoFaForceSms: Boolean = false,
|
||||
val twoFaCode: String? = null,
|
||||
val captchaSid: String? = null,
|
||||
val captchaKey: String? = null,
|
||||
) : Parcelable {
|
||||
|
||||
val map
|
||||
get() = mutableMapOf(
|
||||
"grant_type" to grantType,
|
||||
@@ -28,10 +30,38 @@ data class RequestAuthDirect(
|
||||
"scope" to scope,
|
||||
"2fa_supported" to if (twoFaSupported) "1" else "0",
|
||||
"force_sms" to if (twoFaForceSms) "1" else "0"
|
||||
)
|
||||
)
|
||||
.apply {
|
||||
twoFaCode?.let { this["code"] = it }
|
||||
captchaSid?.let { this["captcha_sid"] = it }
|
||||
captchaKey?.let { this["captcha_key"] = it }
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class AuthWithAppRequest(
|
||||
val redirectUrl: String = "https://oauth.vk.com/blank.html",
|
||||
val display: String = "page",
|
||||
val responseType: String = "token",
|
||||
val accessToken: String,
|
||||
val revoke: Int = 1,
|
||||
val scope: Int = 136297695,
|
||||
val clientId: String = VKConstants.FAST_APP_ID,
|
||||
val sdkPackage: String = BuildConfig.sdkPackage,
|
||||
val sdkFingerprint: String = BuildConfig.sdkFingerprint
|
||||
) : Parcelable {
|
||||
|
||||
val map
|
||||
get() = mutableMapOf(
|
||||
"redirect_url" to redirectUrl,
|
||||
"display" to display,
|
||||
"response_type" to responseType,
|
||||
"access_token" to accessToken,
|
||||
"revoke" to revoke.toString(),
|
||||
"scope" to scope.toString(),
|
||||
"client_id" to clientId,
|
||||
"sdk_package" to sdkPackage,
|
||||
"sdk_fingerprint" to sdkFingerprint
|
||||
)
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class ResponseAuthDirect(
|
||||
data class AuthDirectResponse(
|
||||
@SerializedName("access_token") val accessToken: String? = null,
|
||||
@SerializedName("user_id") val userId: Int? = null,
|
||||
@SerializedName("trusted_hash") val twoFaHash: String? = null,
|
||||
@@ -13,7 +13,7 @@ data class ResponseAuthDirect(
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class ResponseSendSms(
|
||||
data class SendSmsResponse(
|
||||
@SerializedName("sid") val validationSid: String?,
|
||||
@SerializedName("delay") val delay: Int?,
|
||||
@SerializedName("validation_type") val validationType: String?,
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package com.meloda.fast.api.network.longpoll
|
||||
|
||||
import com.meloda.fast.api.base.ApiResponse
|
||||
import com.google.gson.JsonObject
|
||||
import com.meloda.fast.api.network.Answer
|
||||
import org.json.JSONObject
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.QueryMap
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface LongPollRepo {
|
||||
|
||||
@GET("https://{serverUrl}")
|
||||
@GET
|
||||
suspend fun getResponse(
|
||||
@Path("serverUrl") serverUrl: String,
|
||||
@Url serverUrl: String,
|
||||
@QueryMap params: Map<String, String>
|
||||
): Answer<ApiResponse<JSONObject>>
|
||||
): Answer<JsonObject>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.meloda.fast.api.network.longpoll
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class LongPollGetUpdatesRequest(
|
||||
val act: String = "a_check",
|
||||
val key: String,
|
||||
val ts: Int,
|
||||
val wait: Int,
|
||||
val mode: Int
|
||||
) : Parcelable {
|
||||
|
||||
val map
|
||||
get() = mutableMapOf(
|
||||
"act" to act,
|
||||
"key" to key,
|
||||
"ts" to ts.toString(),
|
||||
"wait" to wait.toString(),
|
||||
"mode" to mode.toString()
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,40 +1,50 @@
|
||||
package com.meloda.fast.api.network.messages
|
||||
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest
|
||||
import com.meloda.fast.api.network.longpoll.LongPollRepo
|
||||
import com.meloda.fast.database.dao.MessagesDao
|
||||
import javax.inject.Inject
|
||||
|
||||
class MessagesDataSource @Inject constructor(
|
||||
private val repo: MessagesRepo,
|
||||
private val dao: MessagesDao
|
||||
private val messagesRepo: MessagesRepo,
|
||||
private val messagesDao: MessagesDao,
|
||||
private val longPollRepo: LongPollRepo
|
||||
) {
|
||||
|
||||
suspend fun store(messages: List<VkMessage>) = messagesDao.insert(messages)
|
||||
|
||||
suspend fun getCached(peerId: Int) = messagesDao.getByPeerId(peerId)
|
||||
|
||||
suspend fun getHistory(params: MessagesGetHistoryRequest) =
|
||||
repo.getHistory(params.map)
|
||||
messagesRepo.getHistory(params.map)
|
||||
|
||||
suspend fun send(params: MessagesSendRequest) =
|
||||
repo.send(params.map)
|
||||
messagesRepo.send(params.map)
|
||||
|
||||
suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) =
|
||||
repo.markAsImportant(params.map)
|
||||
messagesRepo.markAsImportant(params.map)
|
||||
|
||||
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
|
||||
repo.getLongPollServer(params.map)
|
||||
messagesRepo.getLongPollServer(params.map)
|
||||
|
||||
suspend fun pin(params: MessagesPinMessageRequest) =
|
||||
repo.pin(params.map)
|
||||
messagesRepo.pin(params.map)
|
||||
|
||||
suspend fun unpin(params: MessagesUnPinMessageRequest) =
|
||||
repo.unpin(params.map)
|
||||
messagesRepo.unpin(params.map)
|
||||
|
||||
suspend fun delete(params: MessagesDeleteRequest) =
|
||||
repo.delete(params.map)
|
||||
messagesRepo.delete(params.map)
|
||||
|
||||
suspend fun edit(params: MessagesEditRequest) =
|
||||
repo.edit(params.map)
|
||||
messagesRepo.edit(params.map)
|
||||
|
||||
suspend fun store(messages: List<VkMessage>) = dao.insert(messages)
|
||||
|
||||
suspend fun getCached(peerId: Int) = dao.getByPeerId(peerId)
|
||||
suspend fun getLongPollUpdates(
|
||||
serverUrl: String,
|
||||
params: LongPollGetUpdatesRequest
|
||||
) = longPollRepo.getResponse(serverUrl, params.map)
|
||||
|
||||
suspend fun getById(params: MessagesGetByIdRequest) =
|
||||
messagesRepo.getById(params.map)
|
||||
}
|
||||
@@ -42,4 +42,8 @@ interface MessagesRepo {
|
||||
@POST(MessagesUrls.Edit)
|
||||
suspend fun edit(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(MessagesUrls.GetById)
|
||||
suspend fun getById(@FieldMap params: Map<String, String>): Answer<ApiResponse<MessagesGetByIdResponse>>
|
||||
|
||||
}
|
||||
@@ -40,7 +40,8 @@ data class MessagesSendRequest(
|
||||
val replyTo: Int? = null,
|
||||
val stickerId: Int? = null,
|
||||
val disableMentions: Boolean? = null,
|
||||
val dontParseLinks: Boolean? = null
|
||||
val dontParseLinks: Boolean? = null,
|
||||
val silent: Boolean? = null
|
||||
) : Parcelable {
|
||||
|
||||
val map
|
||||
@@ -55,6 +56,7 @@ data class MessagesSendRequest(
|
||||
stickerId?.let { this["sticker_id"] = it.toString() }
|
||||
disableMentions?.let { this["disable_mentions"] = it.intString }
|
||||
dontParseLinks?.let { this["dont_parse_links"] = it.intString }
|
||||
silent?.let { this["silent"] = it.toString() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,4 +167,21 @@ data class MessagesEditRequest(
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class MessagesGetByIdRequest(
|
||||
val messagesIds: List<Int>,
|
||||
val extended: Boolean? = null,
|
||||
val fields: String? = null
|
||||
) : Parcelable {
|
||||
|
||||
val map
|
||||
get() = mutableMapOf(
|
||||
"message_ids" to messagesIds.joinToString { it.toString() },
|
||||
).apply {
|
||||
extended?.let { this["extended"] = it.intString }
|
||||
fields?.let { this["fields"] = it }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,8 +10,16 @@ import kotlinx.parcelize.Parcelize
|
||||
@Parcelize
|
||||
data class MessagesGetHistoryResponse(
|
||||
val count: Int,
|
||||
val items: List<BaseVkMessage> = listOf(),
|
||||
val items: List<BaseVkMessage> = emptyList(),
|
||||
val conversations: List<BaseVkConversation>?,
|
||||
val profiles: List<BaseVkUser>?,
|
||||
val groups: List<BaseVkGroup>?
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class MessagesGetByIdResponse(
|
||||
val count: Int,
|
||||
val items: List<BaseVkMessage> = emptyList(),
|
||||
val profiles: List<BaseVkUser>?,
|
||||
val groups: List<BaseVkGroup>?
|
||||
) : Parcelable
|
||||
@@ -13,5 +13,6 @@ object MessagesUrls {
|
||||
const val Unpin = "${VkUrls.API}/messages.unpin"
|
||||
const val Delete = "${VkUrls.API}/messages.delete"
|
||||
const val Edit = "${VkUrls.API}/messages.edit"
|
||||
const val GetById = "${VkUrls.API}/messages.getById"
|
||||
|
||||
}
|
||||
@@ -1,40 +1,12 @@
|
||||
package com.meloda.fast.base
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity, LifecycleOwner {
|
||||
abstract class BaseActivity : AppCompatActivity {
|
||||
|
||||
constructor() : super()
|
||||
|
||||
constructor(@LayoutRes resId: Int) : super(resId)
|
||||
|
||||
protected lateinit var lifecycleRegistry: LifecycleRegistry
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
lifecycleRegistry = LifecycleRegistry(this)
|
||||
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
lifecycleRegistry.currentState = Lifecycle.State.STARTED
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.meloda.fast.base
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
abstract class ResourceManager(protected val context: Context) {
|
||||
|
||||
protected fun getString(@StringRes resId: Int): String {
|
||||
return context.getString(resId)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
protected fun getColor(@ColorRes resId: Int): Int {
|
||||
return ContextCompat.getColor(context, resId)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +1,187 @@
|
||||
package com.meloda.fast.base.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import com.meloda.fast.model.DataItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
abstract class BaseAdapter<Item, VH : BaseHolder>(
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused", "UNCHECKED_CAST")
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
|
||||
var context: Context,
|
||||
values: MutableList<Item>,
|
||||
diffUtil: DiffUtil.ItemCallback<Item>
|
||||
) : ListAdapter<Item, VH>(diffUtil) {
|
||||
diffUtil: DiffUtil.ItemCallback<T>,
|
||||
preAddedValues: List<T> = emptyList(),
|
||||
) : ListAdapter<T, VH>(diffUtil) {
|
||||
|
||||
val cleanValues = mutableListOf<Item>()
|
||||
val values = mutableListOf<Item>()
|
||||
|
||||
init {
|
||||
addAll(values)
|
||||
}
|
||||
protected val adapterScope = CoroutineScope(Dispatchers.Default)
|
||||
private val cleanList = mutableListOf<T>()
|
||||
|
||||
protected var inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
var itemClickListener: ((position: Int) -> Unit) = {}
|
||||
var itemLongClickListener: ((position: Int) -> Boolean) = { false }
|
||||
var itemClickListener: ((position: Int) -> Unit)? = null
|
||||
var itemLongClickListener: ((position: Int) -> Boolean)? = null
|
||||
|
||||
init {
|
||||
cleanList.addAll(preAddedValues)
|
||||
addAll(preAddedValues)
|
||||
}
|
||||
|
||||
fun cloneCurrentList(): MutableList<T> {
|
||||
return ArrayList(currentList)
|
||||
}
|
||||
|
||||
open fun destroy() {}
|
||||
|
||||
override fun getItem(position: Int): Item {
|
||||
return values[position]
|
||||
fun getOrNull(position: Int): T? {
|
||||
return if (position >= 0 && position <= currentList.lastIndex) get(position) else null
|
||||
}
|
||||
|
||||
fun getOrNull(position: Int): Item? {
|
||||
return if (position >= 0 && position <= values.lastIndex) get(position) else null
|
||||
}
|
||||
|
||||
fun getOrElse(position: Int, defaultValue: (Int) -> Item): Item {
|
||||
return if (position >= 0 && position <= values.lastIndex) get(position)
|
||||
fun getOrElse(position: Int, defaultValue: (Int) -> T): T {
|
||||
return if (position >= 0 && position <= currentList.lastIndex) get(position)
|
||||
else defaultValue(position)
|
||||
}
|
||||
|
||||
fun add(position: Int, item: Item) {
|
||||
values.add(position, item)
|
||||
cleanValues.add(position, item)
|
||||
fun add(
|
||||
item: T,
|
||||
position: Int? = null,
|
||||
beforeFooter: Boolean = false,
|
||||
commitCallback: (() -> Unit)? = null
|
||||
) = addAll(listOf(item), position, beforeFooter, commitCallback)
|
||||
|
||||
fun addAll(
|
||||
items: List<T>,
|
||||
position: Int? = null,
|
||||
beforeFooter: Boolean = false,
|
||||
commitCallback: (() -> Unit)? = null
|
||||
) {
|
||||
adapterScope.launch {
|
||||
val newList = cloneCurrentList()
|
||||
if (position == null) {
|
||||
val mutableItems = items.toMutableList()
|
||||
if (beforeFooter && newList.lastOrNull() is DataItem.Footer) {
|
||||
newList.removeLastOrNull()
|
||||
}
|
||||
|
||||
if (beforeFooter) {
|
||||
mutableItems += DataItem.Footer as T
|
||||
}
|
||||
|
||||
newList.addAll(mutableItems)
|
||||
cleanList.addAll(mutableItems)
|
||||
} else {
|
||||
newList.addAll(position, items)
|
||||
cleanList.addAll(position, items)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
submitList(newList, commitCallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun add(item: Item) {
|
||||
values += item
|
||||
cleanValues.add(item)
|
||||
fun remove(item: T, commitCallback: (() -> Unit)? = null) =
|
||||
removeAll(listOf(item), commitCallback)
|
||||
|
||||
fun removeAll(items: List<T>, commitCallback: (() -> Unit)? = null) {
|
||||
val newList = cloneCurrentList()
|
||||
newList.removeAll(items)
|
||||
submitList(newList, commitCallback)
|
||||
|
||||
cleanList.removeAll(items)
|
||||
}
|
||||
|
||||
fun addAll(items: List<Item>) {
|
||||
values += items
|
||||
cleanValues.addAll(items)
|
||||
fun removeAt(index: Int, commitCallback: (() -> Unit)? = null) {
|
||||
val newList = cloneCurrentList()
|
||||
newList.removeAt(index)
|
||||
submitList(newList, commitCallback)
|
||||
|
||||
cleanList.removeAt(index)
|
||||
}
|
||||
|
||||
fun addAll(position: Int, items: List<Item>) {
|
||||
values.addAll(position, items)
|
||||
cleanValues.addAll(position, items)
|
||||
fun clear(commitCallback: (() -> Unit)? = null) = removeAll(currentList, commitCallback)
|
||||
|
||||
fun setItem(
|
||||
item: T,
|
||||
withHeader: Boolean = false,
|
||||
withFooter: Boolean = false,
|
||||
commitCallback: (() -> Unit)? = null
|
||||
) = setItems(listOf(item), withHeader, withFooter, commitCallback)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun setItems(
|
||||
list: List<T>?,
|
||||
withHeader: Boolean = false,
|
||||
withFooter: Boolean = false,
|
||||
commitCallback: (() -> Unit)? = null
|
||||
) {
|
||||
adapterScope.launch {
|
||||
val items = mutableListOf<T>()
|
||||
if (withHeader) items.add(DataItem.Header as T)
|
||||
if (!list.isNullOrEmpty()) items.addAll(list)
|
||||
if (withFooter) items.add(DataItem.Footer as T)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (items == currentList) {
|
||||
refreshList()
|
||||
} else {
|
||||
submitList(items, commitCallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAll(items: List<Item>) {
|
||||
values.removeAll(items)
|
||||
cleanValues.removeAll(items)
|
||||
fun indexOf(item: T): Int {
|
||||
return currentList.indexOf(item)
|
||||
}
|
||||
|
||||
fun removeAt(index: Int) {
|
||||
values.removeAt(index)
|
||||
cleanValues.removeAt(index)
|
||||
val indices get() = currentList.indices
|
||||
|
||||
operator fun get(position: Int): T {
|
||||
return currentList[position]
|
||||
}
|
||||
|
||||
fun remove(item: Item) {
|
||||
values.remove(item)
|
||||
cleanValues.remove(item)
|
||||
operator fun set(position: Int, item: T) = setItem(position, item)
|
||||
|
||||
fun setItem(position: Int, item: T, commitCallback: (() -> Unit)? = null) {
|
||||
val newList = cloneCurrentList()
|
||||
newList[position] = item
|
||||
submitList(newList, commitCallback)
|
||||
|
||||
cleanList[position] = item
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
values.clear()
|
||||
cleanValues.clear()
|
||||
fun isEmpty() = currentList.isEmpty()
|
||||
fun isNotEmpty() = currentList.isNotEmpty()
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun refreshList() {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
operator fun get(position: Int): Item {
|
||||
return values[position]
|
||||
fun updateCleanList(list: List<T>?) {
|
||||
cleanList.clear()
|
||||
list?.run { cleanList.addAll(this) }
|
||||
}
|
||||
|
||||
operator fun set(position: Int, item: Item) {
|
||||
values[position] = item
|
||||
cleanValues[position] = item
|
||||
override fun submitList(list: List<T>?) {
|
||||
super.submitList(list)
|
||||
updateCleanList(list)
|
||||
}
|
||||
|
||||
open fun notifyChanges(oldList: List<Item>, newList: List<Item>) {}
|
||||
|
||||
fun isEmpty() = values.isEmpty()
|
||||
fun isNotEmpty() = values.isNotEmpty()
|
||||
|
||||
fun view(resId: Int, viewGroup: ViewGroup, attachToRoot: Boolean = false): View {
|
||||
return inflater.inflate(resId, viewGroup, attachToRoot)
|
||||
}
|
||||
|
||||
fun updateValues(list: MutableList<Item>) {
|
||||
values.clear()
|
||||
values += list
|
||||
override fun submitList(list: List<T>?, commitCallback: Runnable?) {
|
||||
super.submitList(list, commitCallback)
|
||||
updateCleanList(list)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||
onBindItemViewHolder(holder, position)
|
||||
}
|
||||
|
||||
private fun onBindItemViewHolder(holder: VH, position: Int) {
|
||||
initListeners(holder.itemView, position)
|
||||
holder.bind(position)
|
||||
}
|
||||
@@ -117,15 +189,16 @@ abstract class BaseAdapter<Item, VH : BaseHolder>(
|
||||
protected open fun initListeners(itemView: View, position: Int) {
|
||||
if (itemView is AdapterView<*>) return
|
||||
|
||||
itemView.setOnClickListener { itemClickListener.invoke(position) }
|
||||
itemView.setOnLongClickListener { itemLongClickListener.invoke(position) }
|
||||
itemView.setOnClickListener { itemClickListener?.invoke(position) }
|
||||
itemView.setOnLongClickListener {
|
||||
itemLongClickListener?.invoke(position)
|
||||
return@setOnLongClickListener itemClickListener != null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return values.size
|
||||
return currentList.size
|
||||
}
|
||||
|
||||
val lastPosition
|
||||
get() = itemCount - 1
|
||||
|
||||
}
|
||||
val lastPosition get() = currentList.lastIndex
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.meloda.fast.extensions.dpToPx
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -24,7 +25,7 @@ class EmptyHeaderAdapter(
|
||||
private fun generateHeaderView() = View(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
AndroidUtils.px(56).roundToInt()
|
||||
56.dpToPx()
|
||||
)
|
||||
isClickable = false
|
||||
isEnabled = false
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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
|
||||
|
||||
}
|
||||
@@ -16,4 +16,8 @@ data class ValidationEvent(val sid: String) : VkEvent()
|
||||
object StartProgressEvent : VkEvent()
|
||||
object StopProgressEvent : VkEvent()
|
||||
|
||||
abstract class VkEvent
|
||||
abstract class VkEvent
|
||||
|
||||
interface VkEventCallback<in T : Any> {
|
||||
fun onEvent(event: T)
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class AppGlobal : Application() {
|
||||
lateinit var preferences: SharedPreferences
|
||||
lateinit var resources: Resources
|
||||
lateinit var packageName: String
|
||||
lateinit var instance: AppGlobal
|
||||
private lateinit var instance: AppGlobal
|
||||
|
||||
lateinit var appDatabase: AppDatabase
|
||||
|
||||
@@ -41,6 +41,8 @@ class AppGlobal : Application() {
|
||||
|
||||
var screenWidth = 0
|
||||
var screenHeight = 0
|
||||
|
||||
val Instance get() = instance
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -51,9 +53,7 @@ class AppGlobal : Application() {
|
||||
ACRA.init(this)
|
||||
}
|
||||
|
||||
appDatabase = Room.databaseBuilder(
|
||||
this, AppDatabase::class.java, "cache"
|
||||
)
|
||||
appDatabase = Room.databaseBuilder(this, AppDatabase::class.java, "cache")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
||||
@@ -85,10 +85,8 @@ class AppGlobal : Application() {
|
||||
"width: $screenWidth; height: $screenHeight; density: $density; diagonal: $diagonal; dpiDensity: $densityDpi; scaledDensity: $densityScaled; xDpi: $xDpi; yDpi: $yDpi"
|
||||
)
|
||||
|
||||
|
||||
inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.meloda.fast.common
|
||||
|
||||
import android.os.Bundle
|
||||
import com.github.terrakok.cicerone.androidx.FragmentScreen
|
||||
import com.meloda.fast.screens.conversations.ConversationsFragment
|
||||
import com.meloda.fast.screens.login.LoginFragment
|
||||
import com.meloda.fast.screens.main.MainFragment
|
||||
import com.meloda.fast.screens.messages.MessagesHistoryFragment
|
||||
|
||||
@Suppress("FunctionName")
|
||||
object Screens {
|
||||
fun Main() = FragmentScreen { MainFragment() }
|
||||
fun Login() = FragmentScreen { LoginFragment() }
|
||||
fun Conversations() = FragmentScreen { ConversationsFragment() }
|
||||
fun MessagesHistory(bundle: Bundle) =
|
||||
FragmentScreen { MessagesHistoryFragment.newInstance(bundle) }
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import com.meloda.fast.database.dao.UsersDao
|
||||
VkUser::class,
|
||||
VkGroup::class
|
||||
],
|
||||
version = 26,
|
||||
version = 28,
|
||||
exportSchema = false,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
||||
@@ -5,17 +5,19 @@ import com.google.gson.Gson
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||
import org.json.JSONObject
|
||||
import java.util.stream.Collectors
|
||||
|
||||
@Suppress("UnnecessaryVariable")
|
||||
class Converters {
|
||||
|
||||
private companion object {
|
||||
private const val CACHE_SEPARATOR = "fastkruta228355"
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromListVkMessageToString(messages: List<VkMessage>?): String? {
|
||||
if (messages == null) return null
|
||||
|
||||
val string =
|
||||
messages.map { fromVkMessageToString(it)!! }.stream()
|
||||
.collect(Collectors.joining("fastkruta228355"))
|
||||
val string = messages.map { fromVkMessageToString(it)!! }.joinToString { CACHE_SEPARATOR }
|
||||
|
||||
return string
|
||||
}
|
||||
@@ -24,9 +26,9 @@ class Converters {
|
||||
fun fromStringToListVkMessage(string: String?): List<VkMessage>? {
|
||||
if (string == null) return null
|
||||
|
||||
if (string.contains("fastkruta228355")) {
|
||||
if (string.contains(CACHE_SEPARATOR)) {
|
||||
val messages =
|
||||
string.split("fastkruta228355").map { fromStringToVkMessage(it)!! }
|
||||
string.split(CACHE_SEPARATOR).map { fromStringToVkMessage(it)!! }
|
||||
return messages
|
||||
}
|
||||
|
||||
@@ -55,8 +57,7 @@ class Converters {
|
||||
if (attachments == null) return null
|
||||
|
||||
val string =
|
||||
attachments.map { fromVkAttachmentToString(it)!! }.stream()
|
||||
.collect(Collectors.joining("fastkruta228355"))
|
||||
attachments.map { fromVkAttachmentToString(it)!! }.joinToString { CACHE_SEPARATOR }
|
||||
|
||||
return string
|
||||
}
|
||||
@@ -65,9 +66,9 @@ class Converters {
|
||||
fun fromStringToListVkAttachment(string: String?): List<VkAttachment>? {
|
||||
if (string == null) return null
|
||||
|
||||
if (string.contains("fastkruta228355")) {
|
||||
if (string.contains(CACHE_SEPARATOR)) {
|
||||
val attachments =
|
||||
string.split("fastkruta228355").map { fromStringToVkAttachment(it)!! }
|
||||
string.split(CACHE_SEPARATOR).map { fromStringToVkAttachment(it)!! }
|
||||
return attachments
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.meloda.fast.di
|
||||
|
||||
import com.github.terrakok.cicerone.Cicerone
|
||||
import com.github.terrakok.cicerone.Router
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
object NavigationModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun getCicerone(): Cicerone<Router> = Cicerone.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun getRouter(cicerone: Cicerone<Router>) = cicerone.router
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun getNavigationHolder(cicerone: Cicerone<Router>) = cicerone.getNavigatorHolder()
|
||||
}
|
||||
@@ -2,16 +2,19 @@ package com.meloda.fast.di
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.meloda.fast.api.LongPollUpdatesParser
|
||||
import com.meloda.fast.api.network.AuthInterceptor
|
||||
import com.meloda.fast.api.network.ResultCallFactory
|
||||
import com.meloda.fast.api.network.auth.AuthRepo
|
||||
import com.meloda.fast.api.network.account.AccountDataSource
|
||||
import com.meloda.fast.api.network.account.AccountRepo
|
||||
import com.meloda.fast.api.network.auth.AuthDataSource
|
||||
import com.meloda.fast.api.network.auth.AuthRepo
|
||||
import com.meloda.fast.api.network.conversations.ConversationsDataSource
|
||||
import com.meloda.fast.api.network.conversations.ConversationsRepo
|
||||
import com.meloda.fast.api.network.longpoll.LongPollRepo
|
||||
import com.meloda.fast.api.network.messages.MessagesDataSource
|
||||
import com.meloda.fast.api.network.users.UsersDataSource
|
||||
import com.meloda.fast.api.network.messages.MessagesRepo
|
||||
import com.meloda.fast.api.network.users.UsersDataSource
|
||||
import com.meloda.fast.api.network.users.UsersRepo
|
||||
import com.meloda.fast.database.dao.ConversationsDao
|
||||
import com.meloda.fast.database.dao.MessagesDao
|
||||
@@ -67,22 +70,27 @@ object NetworkModule {
|
||||
fun provideAuthInterceptor(): AuthInterceptor = AuthInterceptor()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthRepo(retrofit: Retrofit): AuthRepo =
|
||||
retrofit.create(AuthRepo::class.java)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideConversationsRepo(retrofit: Retrofit): ConversationsRepo =
|
||||
retrofit.create(ConversationsRepo::class.java)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUsersRepo(retrofit: Retrofit): UsersRepo =
|
||||
retrofit.create(UsersRepo::class.java)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMessagesRepo(retrofit: Retrofit): MessagesRepo =
|
||||
retrofit.create(MessagesRepo::class.java)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLongPollRepo(retrofit: Retrofit): LongPollRepo =
|
||||
retrofit.create(LongPollRepo::class.java)
|
||||
|
||||
@@ -109,7 +117,27 @@ object NetworkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMessagesDataSource(
|
||||
repo: MessagesRepo,
|
||||
dao: MessagesDao
|
||||
): MessagesDataSource = MessagesDataSource(repo, dao)
|
||||
messagesRepo: MessagesRepo,
|
||||
messagesDao: MessagesDao,
|
||||
longPollRepo: LongPollRepo
|
||||
): MessagesDataSource = MessagesDataSource(
|
||||
messagesRepo = messagesRepo,
|
||||
messagesDao = messagesDao,
|
||||
longPollRepo = longPollRepo
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLongPollUpdatesParser(messagesDataSource: MessagesDataSource): LongPollUpdatesParser =
|
||||
LongPollUpdatesParser(messagesDataSource)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountRepo(retrofit: Retrofit): AccountRepo =
|
||||
retrofit.create(AccountRepo::class.java)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountDataSource(repo: AccountRepo): AccountDataSource =
|
||||
AccountDataSource(repo)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.meloda.fast.extensions
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.SparseArray
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.Px
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.view.children
|
||||
|
||||
fun Int.dpToPx(): Int {
|
||||
val metrics = Resources.getSystem().displayMetrics
|
||||
return (this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
|
||||
}
|
||||
|
||||
fun Float.dpToPx(): Int {
|
||||
val metrics = Resources.getSystem().displayMetrics
|
||||
return (this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
|
||||
}
|
||||
|
||||
fun TextView.clear() {
|
||||
text = null
|
||||
}
|
||||
|
||||
fun ViewGroup.saveChildViewStates(): SparseArray<Parcelable> {
|
||||
val childViewStates = SparseArray<Parcelable>()
|
||||
children.forEach { child -> child.saveHierarchyState(childViewStates) }
|
||||
return childViewStates
|
||||
}
|
||||
|
||||
fun ViewGroup.restoreChildViewStates(childViewStates: SparseArray<Parcelable>) {
|
||||
children.forEach { child -> child.restoreHierarchyState(childViewStates) }
|
||||
}
|
||||
|
||||
fun View.invisible() = run { visibility = View.INVISIBLE }
|
||||
|
||||
fun View.visible() = run { visibility = View.VISIBLE }
|
||||
fun View.gone() = run { visibility = View.GONE }
|
||||
|
||||
@JvmOverloads
|
||||
fun View.toggleVisibility(visible: Boolean?, visibilityWhenFalse: Int = View.GONE) =
|
||||
run { visibility = if (visible == true) View.VISIBLE else visibilityWhenFalse }
|
||||
|
||||
fun ValueAnimator.startWithIntValues(from: Int, to: Int) {
|
||||
setIntValues(from, to)
|
||||
start()
|
||||
}
|
||||
|
||||
fun View.setMarginsPx(
|
||||
@Px leftMargin: Int? = null,
|
||||
@Px topMargin: Int? = null,
|
||||
@Px rightMargin: Int? = null,
|
||||
@Px bottomMargin: Int? = null
|
||||
) {
|
||||
if (layoutParams is ViewGroup.MarginLayoutParams) {
|
||||
val params = layoutParams as ViewGroup.MarginLayoutParams
|
||||
leftMargin?.run { params.leftMargin = this }
|
||||
topMargin?.run { params.topMargin = this }
|
||||
rightMargin?.run { params.rightMargin = this }
|
||||
bottomMargin?.run { params.bottomMargin = this }
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, K> Pair<T?, K?>.runIfElementsNotNull(block: (T, K) -> Unit) {
|
||||
val firstCopy = first
|
||||
val secondCopy = second
|
||||
if (firstCopy != null && secondCopy != null) {
|
||||
block(firstCopy, secondCopy)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun ImageView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) {
|
||||
visibility = if (drawable != null) View.VISIBLE else visibilityWhenFalse
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun TextView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) {
|
||||
visibility = if (!text.isNullOrEmpty()) View.VISIBLE else visibilityWhenFalse
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.meloda.fast.extensions
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.widget.ImageView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.Transformation
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.resource.bitmap.*
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
|
||||
object ImageLoader {
|
||||
|
||||
val userAvatarTransformations = listOf(
|
||||
TypeTransformations.CircleCrop
|
||||
)
|
||||
|
||||
fun ImageView.clear() {
|
||||
this.setImageDrawable(null)
|
||||
}
|
||||
|
||||
fun ImageView.loadWithGlide(
|
||||
url: String? = null,
|
||||
uri: Uri? = null,
|
||||
drawableRes: Int? = null,
|
||||
drawable: Drawable? = null,
|
||||
placeholderDrawable: Drawable = ColorDrawable(Color.TRANSPARENT),
|
||||
errorDrawable: Drawable = placeholderDrawable,
|
||||
crossFade: Boolean = false,
|
||||
crossFadeDuration: Int = 200,
|
||||
transformations: List<TypeTransformations> = emptyList(),
|
||||
onLoadedAction: (() -> Unit)? = null,
|
||||
onFailedAction: (() -> Unit)? = null,
|
||||
priority: Priority = Priority.NORMAL,
|
||||
cacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL
|
||||
) {
|
||||
val request = Glide.with(this)
|
||||
|
||||
var builder = when {
|
||||
url != null -> request.load(url)
|
||||
uri != null -> request.load(uri)
|
||||
drawableRes != null -> request.load(drawableRes)
|
||||
drawable != null -> request.load(drawable)
|
||||
else -> request.load(null as Drawable?)
|
||||
}
|
||||
|
||||
builder = builder
|
||||
.apply(TypeTransformations.createRequestOptions(transformations))
|
||||
.error(errorDrawable)
|
||||
.placeholder(placeholderDrawable)
|
||||
.addListener(ImageLoadRequestListener(onLoadedAction, onFailedAction))
|
||||
.diskCacheStrategy(cacheStrategy)
|
||||
.priority(priority)
|
||||
|
||||
if (crossFade) {
|
||||
builder = builder.transition(withCrossFade(crossFadeDuration))
|
||||
}
|
||||
|
||||
builder.into(this)
|
||||
}
|
||||
}
|
||||
|
||||
class ImageLoadRequestListener(
|
||||
private val onLoadedAction: (() -> Unit)?,
|
||||
private val onFailedAction: (() -> Unit)?
|
||||
) : RequestListener<Drawable> {
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
onFailedAction?.invoke()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
onLoadedAction?.invoke()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TypeTransformations {
|
||||
|
||||
object CenterCrop : TypeTransformations()
|
||||
|
||||
object CenterInside : TypeTransformations()
|
||||
|
||||
object CircleCrop : TypeTransformations()
|
||||
|
||||
class RoundedCornerCrop(val radius: Int) : TypeTransformations()
|
||||
|
||||
class GranularRoundedCornerCrop(
|
||||
val topLeft: Float,
|
||||
val topRight: Float,
|
||||
val bottomRight: Float,
|
||||
val bottomLeft: Float
|
||||
) : TypeTransformations()
|
||||
|
||||
fun toGlideTransform(): Transformation<Bitmap> = when (this) {
|
||||
CenterCrop -> CenterCrop()
|
||||
CenterInside -> CenterInside()
|
||||
is RoundedCornerCrop -> RoundedCorners(radius)
|
||||
is GranularRoundedCornerCrop -> GranularRoundedCorners(
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomRight,
|
||||
bottomLeft
|
||||
)
|
||||
CircleCrop -> CircleCrop()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun createRequestOptions(transformations: List<TypeTransformations>): RequestOptions {
|
||||
val mappedTransformations = transformations
|
||||
.map(TypeTransformations::toGlideTransform)
|
||||
.toTypedArray()
|
||||
|
||||
return RequestOptions().transform(* mappedTransformations)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
package com.meloda.fast.extensions
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.collection.forEach
|
||||
import androidx.collection.set
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.api.UserConfig
|
||||
|
||||
/**
|
||||
* Manages the various graphs needed for a [BottomNavigationView].
|
||||
*
|
||||
* This sample is a workaround until the Navigation Component supports multiple back stacks.
|
||||
*/
|
||||
object NavigationExtensions {
|
||||
|
||||
fun BottomNavigationView.setupWithNavController(
|
||||
navGraphIds: List<Int>,
|
||||
fragmentManager: FragmentManager,
|
||||
containerId: Int,
|
||||
intent: Intent
|
||||
): LiveData<NavController> {
|
||||
|
||||
// Map of tags
|
||||
val graphIdToTagMap = SparseArrayCompat<String>()
|
||||
// Result. Mutable live data with the selected controlled
|
||||
val selectedNavController = MutableLiveData<NavController>()
|
||||
|
||||
var firstFragmentGraphId = 0
|
||||
|
||||
// First create a NavHostFragment for each NavGraph ID
|
||||
navGraphIds.forEachIndexed { index, navGraphId ->
|
||||
val fragmentTag = getFragmentTag(index)
|
||||
|
||||
// Find or create the Navigation host fragment
|
||||
val navHostFragment = obtainNavHostFragment(
|
||||
fragmentManager,
|
||||
fragmentTag,
|
||||
navGraphId,
|
||||
containerId
|
||||
)
|
||||
|
||||
// Obtain its id
|
||||
val graphId = navHostFragment.navController.graph.id
|
||||
|
||||
if (index == 0) {
|
||||
firstFragmentGraphId = graphId
|
||||
}
|
||||
|
||||
// Save to the map
|
||||
graphIdToTagMap[graphId] = fragmentTag
|
||||
|
||||
// Attach or detach nav host fragment depending on whether it's the selected item.
|
||||
if (this.selectedItemId == graphId) {
|
||||
// Update livedata with the selected graph
|
||||
selectedNavController.value = navHostFragment.navController
|
||||
attachNavHostFragment(fragmentManager, navHostFragment, index == 0)
|
||||
} else {
|
||||
detachNavHostFragment(fragmentManager, navHostFragment)
|
||||
}
|
||||
}
|
||||
|
||||
// Now connect selecting an item with swapping Fragments
|
||||
var selectedItemTag = graphIdToTagMap[this.selectedItemId]
|
||||
val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId] ?: ""
|
||||
var isOnFirstFragment = selectedItemTag == firstFragmentTag
|
||||
|
||||
setOnItemSelectedListener { item ->
|
||||
// Don't do anything if the state is state has already been saved.
|
||||
if (fragmentManager.isStateSaved) {
|
||||
false
|
||||
} else {
|
||||
val navController =
|
||||
(fragmentManager.findFragmentByTag(selectedItemTag) as NavHostFragment).navController
|
||||
navController.popBackStack(navController.graph.startDestination, false)
|
||||
if (selectedItemTag != graphIdToTagMap[item.itemId]) {
|
||||
val newlySelectedItemTag = //graphIdToTagMap[item.itemId]
|
||||
if (!UserConfig.isLoggedIn()) graphIdToTagMap[R.id.login] else graphIdToTagMap[item.itemId]
|
||||
|
||||
fragmentManager.popBackStack(
|
||||
firstFragmentTag,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE
|
||||
)
|
||||
val selectedFragment =
|
||||
fragmentManager.findFragmentByTag(newlySelectedItemTag) as NavHostFragment
|
||||
|
||||
// Exclude the first fragment tag because it's always in the back stack.
|
||||
if (firstFragmentTag != newlySelectedItemTag) {
|
||||
// Commit a transaction that cleans the back stack and adds the first fragment
|
||||
// to it, creating the fixed started destination.
|
||||
fragmentManager.beginTransaction()
|
||||
.setCustomAnimations(
|
||||
R.anim.nav_default_enter_anim,
|
||||
R.anim.nav_default_exit_anim,
|
||||
R.anim.nav_default_pop_enter_anim,
|
||||
R.anim.nav_default_pop_exit_anim
|
||||
)
|
||||
.attach(selectedFragment)
|
||||
.setPrimaryNavigationFragment(selectedFragment)
|
||||
.apply {
|
||||
// Detach all other Fragments
|
||||
graphIdToTagMap.forEach { _, fragmentTagIter ->
|
||||
if (fragmentTagIter != newlySelectedItemTag) {
|
||||
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
.addToBackStack(firstFragmentTag)
|
||||
.setReorderingAllowed(true)
|
||||
.commit()
|
||||
}
|
||||
selectedItemTag = newlySelectedItemTag
|
||||
isOnFirstFragment = selectedItemTag == firstFragmentTag
|
||||
selectedNavController.value = selectedFragment.navController
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setOnItemReselectedListener { item ->
|
||||
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
|
||||
val selectedFragment =
|
||||
fragmentManager.findFragmentByTag(newlySelectedItemTag) as NavHostFragment
|
||||
val navController = selectedFragment.navController
|
||||
// Pop the back stack to the start destination of the current navController graph
|
||||
if (selectedItemTag != graphIdToTagMap[item.itemId]) {
|
||||
fragmentManager.beginTransaction()
|
||||
.setCustomAnimations(
|
||||
R.anim.nav_default_enter_anim,
|
||||
R.anim.nav_default_exit_anim,
|
||||
R.anim.nav_default_pop_enter_anim,
|
||||
R.anim.nav_default_pop_exit_anim
|
||||
)
|
||||
.attach(selectedFragment)
|
||||
.setPrimaryNavigationFragment(selectedFragment)
|
||||
.apply {
|
||||
// Detach all other Fragments
|
||||
graphIdToTagMap.forEach { _, fragmentTagIter ->
|
||||
if (fragmentTagIter != newlySelectedItemTag) {
|
||||
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
.addToBackStack(firstFragmentTag)
|
||||
.setReorderingAllowed(true)
|
||||
.commit()
|
||||
selectedItemTag = newlySelectedItemTag
|
||||
isOnFirstFragment = selectedItemTag == firstFragmentTag
|
||||
selectedNavController.value = selectedFragment.navController
|
||||
} else navController.popBackStack(navController.graph.startDestination, false)
|
||||
}
|
||||
// Optional: on item reselected, pop back stack to the destination of the graph
|
||||
setupDeepLinks(navGraphIds, fragmentManager, containerId, intent)
|
||||
|
||||
// Finally, ensure that we update our BottomNavigationView when the back stack changes
|
||||
fragmentManager.addOnBackStackChangedListener {
|
||||
if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) {
|
||||
this.selectedItemId = firstFragmentGraphId
|
||||
}
|
||||
|
||||
// Reset the graph if the currentDestination is not valid (happens when the back
|
||||
// stack is popped after using the back button).
|
||||
selectedNavController.value?.let { controller ->
|
||||
if (controller.currentDestination == null) {
|
||||
controller.navigate(controller.graph.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedNavController
|
||||
}
|
||||
|
||||
private fun BottomNavigationView.setupDeepLinks(
|
||||
navGraphIds: List<Int>,
|
||||
fragmentManager: FragmentManager,
|
||||
containerId: Int,
|
||||
intent: Intent
|
||||
) {
|
||||
navGraphIds.forEachIndexed { index, navGraphId ->
|
||||
val fragmentTag = getFragmentTag(index)
|
||||
|
||||
// Find or create the Navigation host fragment
|
||||
val navHostFragment = obtainNavHostFragment(
|
||||
fragmentManager,
|
||||
fragmentTag,
|
||||
navGraphId,
|
||||
containerId
|
||||
)
|
||||
// Handle Intent
|
||||
if (navHostFragment.navController.handleDeepLink(intent) &&
|
||||
selectedItemId != navHostFragment.navController.graph.id
|
||||
) {
|
||||
this.selectedItemId = navHostFragment.navController.graph.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun detachNavHostFragment(
|
||||
fragmentManager: FragmentManager,
|
||||
navHostFragment: NavHostFragment
|
||||
) {
|
||||
fragmentManager.beginTransaction()
|
||||
.detach(navHostFragment)
|
||||
.commitNow()
|
||||
}
|
||||
|
||||
private fun attachNavHostFragment(
|
||||
fragmentManager: FragmentManager,
|
||||
navHostFragment: NavHostFragment,
|
||||
isPrimaryNavFragment: Boolean
|
||||
) {
|
||||
fragmentManager.beginTransaction()
|
||||
.attach(navHostFragment)
|
||||
.apply {
|
||||
if (isPrimaryNavFragment) {
|
||||
setPrimaryNavigationFragment(navHostFragment)
|
||||
}
|
||||
}
|
||||
.commitNow()
|
||||
}
|
||||
|
||||
private fun obtainNavHostFragment(
|
||||
fragmentManager: FragmentManager,
|
||||
fragmentTag: String,
|
||||
navGraphId: Int,
|
||||
containerId: Int,
|
||||
): NavHostFragment {
|
||||
// If the Nav Host fragment exists, return it
|
||||
val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
|
||||
existingFragment?.let { return it }
|
||||
|
||||
// Otherwise, create it and return it.
|
||||
val navHostFragment = NavHostFragment.create(navGraphId)
|
||||
fragmentManager.beginTransaction()
|
||||
.add(containerId, navHostFragment, fragmentTag)
|
||||
.commitNow()
|
||||
return navHostFragment
|
||||
}
|
||||
|
||||
private fun FragmentManager.isOnBackStack(backStackName: String): Boolean {
|
||||
val backStackCount = backStackEntryCount
|
||||
for (index in 0 until backStackCount) {
|
||||
if (getBackStackEntryAt(index).name == backStackName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
val FragmentManager.visibleFragments
|
||||
get(): List<Fragment> {
|
||||
val visibleFragments = arrayListOf<Fragment>()
|
||||
fragments.forEach { if (it.isVisible) visibleFragments.add(it) }
|
||||
return visibleFragments
|
||||
}
|
||||
|
||||
private fun getFragmentTag(index: Int) = "bottomNavigation#$index"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.meloda.fast.extensions
|
||||
|
||||
import android.widget.TextView
|
||||
|
||||
object TextViewExtensions {
|
||||
|
||||
fun TextView.clear() {
|
||||
text = null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.meloda.fast.model
|
||||
|
||||
sealed class DataItem<IdType> {
|
||||
abstract val dataItemId: IdType
|
||||
|
||||
object Header : DataItem<Int>() {
|
||||
override val dataItemId = Int.MIN_VALUE
|
||||
}
|
||||
|
||||
object Footer : DataItem<Int>() {
|
||||
override val dataItemId = Int.MIN_VALUE + 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.meloda.fast.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Ignore
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
open class SelectableItem constructor(
|
||||
@Ignore
|
||||
val selectableItemId: Int = 0
|
||||
) : DataItem<Int>(), Parcelable {
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var isSelected: Boolean = false
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
override val dataItemId = selectableItemId
|
||||
|
||||
}
|
||||
@@ -8,10 +8,10 @@ import android.text.TextUtils
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.ObjectsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import coil.load
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.VKConstants
|
||||
@@ -22,26 +22,35 @@ import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.base.adapter.BaseAdapter
|
||||
import com.meloda.fast.base.adapter.BindingHolder
|
||||
import com.meloda.fast.databinding.ItemConversationBinding
|
||||
import com.meloda.fast.extensions.ImageLoader
|
||||
import com.meloda.fast.extensions.ImageLoader.clear
|
||||
import com.meloda.fast.extensions.ImageLoader.loadWithGlide
|
||||
import com.meloda.fast.extensions.gone
|
||||
import com.meloda.fast.extensions.toggleVisibility
|
||||
import com.meloda.fast.extensions.visible
|
||||
import com.meloda.fast.util.TimeUtils
|
||||
|
||||
class ConversationsAdapter constructor(
|
||||
context: Context,
|
||||
values: MutableList<VkConversation>,
|
||||
private val resourceManager: ConversationsResourceManager,
|
||||
var isMultilineEnabled: Boolean = true,
|
||||
val profiles: HashMap<Int, VkUser> = hashMapOf(),
|
||||
val groups: HashMap<Int, VkGroup> = hashMapOf(),
|
||||
var isMultilineEnabled: Boolean = true
|
||||
) : BaseAdapter<VkConversation, ConversationsAdapter.ItemHolder>(
|
||||
context, values, COMPARATOR
|
||||
) {
|
||||
) : BaseAdapter<VkConversation, ConversationsAdapter.ItemHolder>(context, Comparator) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
ItemHolder(ItemConversationBinding.inflate(inflater, parent, false))
|
||||
var pinnedCount = 0
|
||||
|
||||
inner class ItemHolder(binding: ItemConversationBinding) :
|
||||
BindingHolder<ItemConversationBinding>(binding) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder {
|
||||
return ItemHolder(
|
||||
ItemConversationBinding.inflate(inflater, parent, false),
|
||||
resourceManager
|
||||
)
|
||||
}
|
||||
|
||||
private val dateColor = ContextCompat.getColor(context, R.color.n2_500)
|
||||
private val youPrefix = context.getString(R.string.you_message_prefix)
|
||||
inner class ItemHolder(
|
||||
binding: ItemConversationBinding,
|
||||
private val resourceManager: ConversationsResourceManager
|
||||
) : BindingHolder<ItemConversationBinding>(binding) {
|
||||
|
||||
init {
|
||||
binding.title.ellipsize = TextUtils.TruncateAt.END
|
||||
@@ -69,7 +78,7 @@ class ConversationsAdapter constructor(
|
||||
)
|
||||
|
||||
val span = SpannableString(text)
|
||||
span.setSpan(ForegroundColorSpan(dateColor), 0, text.length, 0)
|
||||
span.setSpan(ForegroundColorSpan(resourceManager.colorOutline), 0, text.length, 0)
|
||||
|
||||
binding.message.text = span
|
||||
return
|
||||
@@ -87,51 +96,46 @@ class ConversationsAdapter constructor(
|
||||
conversationGroup = conversationGroup
|
||||
)
|
||||
|
||||
binding.avatar.isVisible = avatar != null
|
||||
binding.avatar.toggleVisibility(avatar != null)
|
||||
|
||||
if (avatar == null) {
|
||||
binding.avatarPlaceholder.isVisible = true
|
||||
binding.avatarPlaceholder.visible()
|
||||
|
||||
if (conversation.ownerId == VKConstants.FAST_GROUP_ID) {
|
||||
binding.placeholderBack.setImageDrawable(
|
||||
ColorDrawable(
|
||||
ContextCompat.getColor(context, R.color.a1_400)
|
||||
)
|
||||
binding.placeholderBack.loadWithGlide(
|
||||
drawable = ColorDrawable(resourceManager.icLauncherColor),
|
||||
transformations = ImageLoader.userAvatarTransformations
|
||||
)
|
||||
binding.placeholder.imageTintList =
|
||||
ColorStateList.valueOf(ContextCompat.getColor(context, R.color.a1_0))
|
||||
ColorStateList.valueOf(resourceManager.colorOnPrimary)
|
||||
binding.placeholder.setImageResource(R.drawable.ic_fast_logo)
|
||||
binding.placeholder.setPadding(18)
|
||||
} else {
|
||||
binding.placeholderBack.setImageDrawable(
|
||||
ColorDrawable(
|
||||
ContextCompat.getColor(context, R.color.n1_50)
|
||||
)
|
||||
binding.placeholderBack.loadWithGlide(
|
||||
drawable = ColorDrawable(resourceManager.colorOnUserAvatarAction),
|
||||
transformations = ImageLoader.userAvatarTransformations
|
||||
)
|
||||
binding.placeholder.imageTintList =
|
||||
ColorStateList.valueOf(ContextCompat.getColor(context, R.color.n2_500))
|
||||
ColorStateList.valueOf(resourceManager.colorUserAvatarAction)
|
||||
binding.placeholder.setImageResource(R.drawable.ic_account_circle_cut)
|
||||
binding.placeholder.setPadding(0)
|
||||
binding.avatar.setImageDrawable(null)
|
||||
binding.avatar.clear()
|
||||
}
|
||||
} else {
|
||||
binding.avatar.load(avatar) {
|
||||
crossfade(200)
|
||||
target {
|
||||
binding.avatarPlaceholder.isVisible = false
|
||||
binding.avatar.setImageDrawable(it)
|
||||
}
|
||||
}
|
||||
binding.avatar.loadWithGlide(
|
||||
url = avatar,
|
||||
crossFade = true,
|
||||
onLoadedAction = { binding.avatarPlaceholder.gone() }
|
||||
)
|
||||
}
|
||||
|
||||
binding.online.isVisible = conversationUser?.online == true
|
||||
|
||||
binding.pin.isVisible = conversation.isPinned
|
||||
binding.online.toggleVisibility(conversationUser?.online == true)
|
||||
binding.pin.toggleVisibility(conversation.isPinned)
|
||||
|
||||
val actionMessage = VkUtils.getActionConversationText(
|
||||
context = context,
|
||||
message = message,
|
||||
youPrefix = youPrefix,
|
||||
youPrefix = resourceManager.youPrefix,
|
||||
profiles = profiles,
|
||||
groups = groups,
|
||||
messageUser = messageUser,
|
||||
@@ -150,7 +154,7 @@ class ConversationsAdapter constructor(
|
||||
message = message
|
||||
)
|
||||
|
||||
binding.textAttachment.isVisible = attachmentIcon != null
|
||||
binding.textAttachment.toggleVisibility(attachmentIcon != null)
|
||||
binding.textAttachment.setImageDrawable(attachmentIcon)
|
||||
|
||||
val attachmentText = if (attachmentIcon == null) VkUtils.getAttachmentText(
|
||||
@@ -174,7 +178,7 @@ class ConversationsAdapter constructor(
|
||||
|
||||
var prefix = when {
|
||||
actionMessage != null -> ""
|
||||
message.isOut -> "$youPrefix: "
|
||||
message.isOut -> "${resourceManager.youPrefix}: "
|
||||
else -> {
|
||||
if (message.isUser() && messageUser != null && messageUser.firstName.isNotBlank()) "${messageUser.firstName}: "
|
||||
else if (message.isGroup() && messageGroup != null && messageGroup.name.isNotBlank()) "${messageGroup.name}: "
|
||||
@@ -190,7 +194,7 @@ class ConversationsAdapter constructor(
|
||||
|
||||
val spanMessage = SpannableString(spanText)
|
||||
spanMessage.setSpan(
|
||||
ForegroundColorSpan(dateColor), 0,
|
||||
ForegroundColorSpan(resourceManager.colorOutline), 0,
|
||||
prefix.length + coloredMessage.length,
|
||||
0
|
||||
)
|
||||
@@ -208,6 +212,15 @@ class ConversationsAdapter constructor(
|
||||
R.drawable.ic_message_unread
|
||||
) else null
|
||||
|
||||
binding.onlineBorder.setImageDrawable(
|
||||
ColorDrawable(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
if (conversation.isUnread()) R.color.colorBackgroundVariant
|
||||
else R.color.colorBackground
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
binding.counter.isVisible = conversation.isInUnread()
|
||||
if (conversation.isInUnread()) {
|
||||
@@ -222,10 +235,10 @@ class ConversationsAdapter constructor(
|
||||
}
|
||||
|
||||
fun removeConversation(conversationId: Int): Int? {
|
||||
for (i in values.indices) {
|
||||
val conversation = values[i]
|
||||
for (i in indices) {
|
||||
val conversation = getItem(i)
|
||||
if (conversation.id == conversationId) {
|
||||
values.removeAt(i)
|
||||
removeAt(i)
|
||||
return i
|
||||
}
|
||||
}
|
||||
@@ -233,17 +246,29 @@ class ConversationsAdapter constructor(
|
||||
return null
|
||||
}
|
||||
|
||||
fun searchConversationIndex(conversationId: Int): Int? {
|
||||
for (i in indices) {
|
||||
val conversation = getItem(i)
|
||||
|
||||
if (conversation.id == conversationId) return i
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val COMPARATOR = object : DiffUtil.ItemCallback<VkConversation>() {
|
||||
private val Comparator = object : DiffUtil.ItemCallback<VkConversation>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: VkConversation,
|
||||
newItem: VkConversation
|
||||
) = false
|
||||
): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: VkConversation,
|
||||
newItem: VkConversation
|
||||
) = false
|
||||
) = ObjectsCompat.equals(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+77
-77
@@ -1,23 +1,16 @@
|
||||
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.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.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
|
||||
@@ -28,14 +21,16 @@ import com.meloda.fast.common.AppGlobal
|
||||
import com.meloda.fast.common.AppSettings
|
||||
import com.meloda.fast.common.dataStore
|
||||
import com.meloda.fast.databinding.FragmentConversationsBinding
|
||||
import com.meloda.fast.extensions.ImageLoader.loadWithGlide
|
||||
import com.meloda.fast.extensions.gone
|
||||
import com.meloda.fast.extensions.toggleVisibility
|
||||
import com.meloda.fast.screens.messages.MessagesHistoryFragment
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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 :
|
||||
@@ -47,9 +42,7 @@ class ConversationsFragment :
|
||||
private val adapter: ConversationsAdapter by lazy {
|
||||
ConversationsAdapter(
|
||||
requireContext(),
|
||||
mutableListOf(),
|
||||
hashMapOf(),
|
||||
hashMapOf()
|
||||
ConversationsResourceManager(requireContext())
|
||||
).also {
|
||||
it.itemClickListener = this::onItemClick
|
||||
it.itemLongClickListener = this::onItemLongClick
|
||||
@@ -74,13 +67,6 @@ class ConversationsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private var isPaused = false
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
isPaused = true
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
prepareViews()
|
||||
@@ -90,41 +76,16 @@ class ConversationsFragment :
|
||||
lifecycleScope.launch {
|
||||
requireContext().dataStore.data.map {
|
||||
adapter.isMultilineEnabled = it[AppSettings.keyIsMultilineEnabled] ?: true
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
adapter.refreshList()
|
||||
}.collect()
|
||||
}
|
||||
|
||||
binding.createChat.setOnClickListener {}
|
||||
|
||||
UserConfig.vkUser.observe(viewLifecycleOwner) {
|
||||
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } }
|
||||
UserConfig.vkUser.observe(viewLifecycleOwner) { user ->
|
||||
user?.run { binding.avatar.loadWithGlide(url = this.photo200, crossFade = true) }
|
||||
}
|
||||
|
||||
binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
|
||||
if (isPaused) return@OnOffsetChangedListener
|
||||
|
||||
binding.appBar.animate().translationZ(
|
||||
if (verticalOffset < 0) AndroidUtils.px(3).roundToInt().toFloat()
|
||||
else 0f
|
||||
).setDuration(50).start()
|
||||
|
||||
val padding = AndroidUtils.px(if (verticalOffset <= -100) 10 else 30).roundToInt()
|
||||
|
||||
binding.avatarContainer.updatePadding(
|
||||
bottom = padding,
|
||||
right = padding
|
||||
)
|
||||
|
||||
val minusAlpha = (1 - (abs(verticalOffset) * 0.01)).toFloat()
|
||||
val plusAlpha = (abs(1 + verticalOffset * 0.01) * 1.01).toFloat()
|
||||
|
||||
println("Fast::ConversationsFragment::onOffset offset: $verticalOffset; minusAlpha: $minusAlpha; plusAlpha: $plusAlpha")
|
||||
|
||||
val alpha: Float = if (verticalOffset <= -100) plusAlpha else minusAlpha
|
||||
|
||||
binding.avatarContainer.alpha = alpha
|
||||
})
|
||||
|
||||
binding.avatar.setOnClickListener { avatarPopupMenu.show() }
|
||||
|
||||
binding.avatar.setOnLongClickListener {
|
||||
@@ -134,23 +95,18 @@ class ConversationsFragment :
|
||||
settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled
|
||||
|
||||
adapter.isMultilineEnabled = !isMultilineEnabled
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
adapter.refreshList()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
if (isPaused) {
|
||||
isPaused = false
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.loadProfileUser()
|
||||
viewModel.loadConversations()
|
||||
}
|
||||
|
||||
private fun showLogOutDialog() {
|
||||
val isEasterEgg = UserConfig.userId == UserConfig.userId
|
||||
val isEasterEgg = UserConfig.userId == 37610580
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(
|
||||
@@ -166,13 +122,7 @@ class ConversationsFragment :
|
||||
UserConfig.clear()
|
||||
AppGlobal.appDatabase.clearAllTables()
|
||||
|
||||
requireActivity().finishAffinity()
|
||||
requireActivity().startActivity(
|
||||
Intent(
|
||||
requireContext(),
|
||||
MainActivity::class.java
|
||||
)
|
||||
)
|
||||
viewModel.openRootScreen()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
@@ -185,21 +135,31 @@ class ConversationsFragment :
|
||||
is StartProgressEvent -> onProgressStarted()
|
||||
is StopProgressEvent -> onProgressStopped()
|
||||
|
||||
is ConversationsLoaded -> refreshConversations(event)
|
||||
is ConversationsDelete -> deleteConversation(event.peerId)
|
||||
is ConversationsLoadedEvent -> refreshConversations(event)
|
||||
is ConversationsDeleteEvent -> deleteConversation(event.peerId)
|
||||
|
||||
// TODO: 10-Oct-21 remove this and sort conversations list
|
||||
is ConversationsPin, is ConversationsUnpin -> viewModel.loadConversations()
|
||||
is ConversationsPinEvent -> {
|
||||
adapter.pinnedCount++
|
||||
viewModel.loadConversations()
|
||||
}
|
||||
is ConversationsUnpinEvent -> {
|
||||
adapter.pinnedCount--
|
||||
viewModel.loadConversations()
|
||||
}
|
||||
|
||||
is MessagesNewEvent -> onMessageNew(event)
|
||||
is MessagesEditEvent -> onMessageEdit(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProgressStarted() {
|
||||
binding.progressBar.isVisible = adapter.isEmpty()
|
||||
binding.progressBar.toggleVisibility(adapter.isEmpty())
|
||||
binding.refreshLayout.isRefreshing = adapter.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun onProgressStopped() {
|
||||
binding.progressBar.isVisible = false
|
||||
binding.progressBar.gone()
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
@@ -233,16 +193,17 @@ class ConversationsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshConversations(event: ConversationsLoaded) {
|
||||
private fun refreshConversations(event: ConversationsLoadedEvent) {
|
||||
adapter.profiles += event.profiles
|
||||
adapter.groups += event.groups
|
||||
|
||||
val pinnedConversations = event.conversations.filter { it.isPinned }
|
||||
adapter.pinnedCount = pinnedConversations.count()
|
||||
|
||||
fillRecyclerView(event.conversations)
|
||||
}
|
||||
|
||||
private fun fillRecyclerView(values: List<VkConversation>) {
|
||||
adapter.values.clear()
|
||||
adapter.values += values
|
||||
adapter.submitList(values)
|
||||
}
|
||||
|
||||
@@ -257,12 +218,11 @@ class ConversationsFragment :
|
||||
if (conversation.isGroup()) adapter.groups[conversation.id]
|
||||
else null
|
||||
|
||||
findNavController().navigate(
|
||||
R.id.toMessagesHistory,
|
||||
viewModel.openMessagesHistoryScreen(
|
||||
bundleOf(
|
||||
"conversation" to adapter[position],
|
||||
"user" to user,
|
||||
"group" to group
|
||||
MessagesHistoryFragment.ARG_USER to user,
|
||||
MessagesHistoryFragment.ARG_GROUP to group,
|
||||
MessagesHistoryFragment.ARG_CONVERSATION to conversation
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -277,7 +237,7 @@ class ConversationsFragment :
|
||||
|
||||
var canPinOneMoreDialog = true
|
||||
if (adapter.itemCount > 4) {
|
||||
val firstFiveDialogs = adapter.values.subList(0, 5)
|
||||
val firstFiveDialogs = adapter.currentList.subList(0, 5)
|
||||
var pinnedCount = 0
|
||||
|
||||
firstFiveDialogs.forEach { if (it.isPinned) pinnedCount++ }
|
||||
@@ -321,8 +281,7 @@ class ConversationsFragment :
|
||||
}
|
||||
|
||||
private fun deleteConversation(conversationId: Int) {
|
||||
val index = adapter.removeConversation(conversationId) ?: return
|
||||
adapter.notifyItemRemoved(index)
|
||||
adapter.removeConversation(conversationId)
|
||||
}
|
||||
|
||||
private fun showPinConversationDialog(conversation: VkConversation) {
|
||||
@@ -345,4 +304,45 @@ class ConversationsFragment :
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onMessageNew(event: MessagesNewEvent) {
|
||||
adapter.profiles += event.profiles
|
||||
adapter.groups += event.groups
|
||||
|
||||
val message = event.message
|
||||
|
||||
val conversationIndex = adapter.searchConversationIndex(message.peerId)
|
||||
if (conversationIndex == null) { // диалога нет в списке
|
||||
|
||||
} else {
|
||||
val conversation = adapter[conversationIndex]
|
||||
conversation.run {
|
||||
lastMessage = message
|
||||
lastMessageId = message.id
|
||||
lastConversationMessageId = -1
|
||||
}
|
||||
|
||||
if (conversation.isPinned) {
|
||||
adapter[conversationIndex] = conversation
|
||||
return
|
||||
}
|
||||
|
||||
adapter.removeConversation(message.peerId) ?: return
|
||||
val toPosition = adapter.pinnedCount
|
||||
|
||||
adapter.add(conversation, toPosition)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMessageEdit(event: MessagesEditEvent) {
|
||||
val message = event.message
|
||||
|
||||
val conversationIndex = adapter.searchConversationIndex(message.peerId)
|
||||
if (conversationIndex == null) { // диалога нет в списке
|
||||
|
||||
} else {
|
||||
val conversation = adapter[conversationIndex]
|
||||
conversation.lastMessage = message
|
||||
adapter[conversationIndex] = conversation
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.meloda.fast.screens.conversations
|
||||
|
||||
import android.content.Context
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.base.ResourceManager
|
||||
import com.meloda.fast.extensions.TypeTransformations
|
||||
|
||||
class ConversationsResourceManager(context: Context) : ResourceManager(context) {
|
||||
|
||||
val colorOutline = getColor(R.color.colorOutline)
|
||||
val colorOnPrimary = getColor(R.color.colorOnPrimary)
|
||||
val colorUserAvatarAction = getColor(R.color.colorUserAvatarAction)
|
||||
val colorOnUserAvatarAction = getColor(R.color.colorOnUserAvatarAction)
|
||||
|
||||
val icLauncherColor = getColor(R.color.a1_500)
|
||||
|
||||
val youPrefix = getString(R.string.you_message_prefix)
|
||||
|
||||
}
|
||||
+62
-10
@@ -1,28 +1,49 @@
|
||||
package com.meloda.fast.screens.conversations
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.terrakok.cicerone.Router
|
||||
import com.meloda.fast.api.LongPollEvent
|
||||
import com.meloda.fast.api.LongPollUpdatesParser
|
||||
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
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.api.network.conversations.*
|
||||
import com.meloda.fast.api.network.users.UsersDataSource
|
||||
import com.meloda.fast.api.network.users.UsersGetRequest
|
||||
import com.meloda.fast.base.viewmodel.BaseViewModel
|
||||
import com.meloda.fast.base.viewmodel.VkEvent
|
||||
import com.meloda.fast.common.Screens
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ConversationsViewModel @Inject constructor(
|
||||
private val conversations: ConversationsDataSource,
|
||||
private val users: UsersDataSource
|
||||
private val users: UsersDataSource,
|
||||
updatesParser: LongPollUpdatesParser,
|
||||
private val router: Router
|
||||
) : BaseViewModel() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ConversationsViewModel"
|
||||
}
|
||||
|
||||
init {
|
||||
updatesParser.onNewMessage {
|
||||
viewModelScope.launch { handleNewMessage(it) }
|
||||
}
|
||||
|
||||
updatesParser.onMessageEdited {
|
||||
viewModelScope.launch { handleEditedMessage(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun loadConversations(
|
||||
offset: Int? = null
|
||||
) = viewModelScope.launch(Dispatchers.Default) {
|
||||
@@ -49,7 +70,7 @@ class ConversationsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
sendEvent(
|
||||
ConversationsLoaded(
|
||||
ConversationsLoadedEvent(
|
||||
count = response.count,
|
||||
offset = offset,
|
||||
unreadCount = response.unreadCount ?: 0,
|
||||
@@ -84,7 +105,7 @@ class ConversationsViewModel @Inject constructor(
|
||||
conversations.delete(
|
||||
ConversationsDeleteRequest(peerId)
|
||||
)
|
||||
}, onAnswer = { sendEvent(ConversationsDelete(peerId)) })
|
||||
}, onAnswer = { sendEvent(ConversationsDeleteEvent(peerId)) })
|
||||
}
|
||||
|
||||
fun pinConversation(
|
||||
@@ -94,18 +115,41 @@ class ConversationsViewModel @Inject constructor(
|
||||
if (pin) {
|
||||
makeJob(
|
||||
{ conversations.pin(ConversationsPinRequest(peerId)) },
|
||||
onAnswer = { sendEvent(ConversationsPin(peerId)) }
|
||||
onAnswer = { sendEvent(ConversationsPinEvent(peerId)) }
|
||||
)
|
||||
} else {
|
||||
makeJob(
|
||||
{ conversations.unpin(ConversationsUnpinRequest(peerId)) },
|
||||
onAnswer = { sendEvent(ConversationsUnpin(peerId)) }
|
||||
onAnswer = { sendEvent(ConversationsUnpinEvent(peerId)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) {
|
||||
sendEvent(
|
||||
MessagesNewEvent(
|
||||
message = event.message,
|
||||
profiles = event.profiles,
|
||||
groups = event.groups
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun handleEditedMessage(event: LongPollEvent.VkMessageEditEvent) {
|
||||
sendEvent(MessagesEditEvent(event.message))
|
||||
}
|
||||
|
||||
fun openRootScreen() {
|
||||
router.exit()
|
||||
router.newRootScreen(Screens.Main())
|
||||
}
|
||||
|
||||
fun openMessagesHistoryScreen(bundle: Bundle) {
|
||||
router.navigateTo(Screens.MessagesHistory(bundle))
|
||||
}
|
||||
}
|
||||
|
||||
data class ConversationsLoaded(
|
||||
data class ConversationsLoadedEvent(
|
||||
val count: Int,
|
||||
val offset: Int?,
|
||||
val unreadCount: Int?,
|
||||
@@ -114,8 +158,16 @@ data class ConversationsLoaded(
|
||||
val groups: HashMap<Int, VkGroup>
|
||||
) : VkEvent()
|
||||
|
||||
data class ConversationsDelete(val peerId: Int) : VkEvent()
|
||||
data class ConversationsDeleteEvent(val peerId: Int) : VkEvent()
|
||||
|
||||
data class ConversationsPin(val peerId: Int) : VkEvent()
|
||||
data class ConversationsPinEvent(val peerId: Int) : VkEvent()
|
||||
|
||||
data class ConversationsUnpin(val peerId: Int) : VkEvent()
|
||||
data class ConversationsUnpinEvent(val peerId: Int) : VkEvent()
|
||||
|
||||
data class MessagesNewEvent(
|
||||
val message: VkMessage,
|
||||
val profiles: HashMap<Int, VkUser>,
|
||||
val groups: HashMap<Int, VkGroup>
|
||||
) : VkEvent()
|
||||
|
||||
data class MessagesEditEvent(val message: VkMessage) : VkEvent()
|
||||
@@ -18,7 +18,6 @@ import androidx.core.view.isVisible
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
@@ -35,9 +34,7 @@ import com.meloda.fast.databinding.FragmentLoginBinding
|
||||
import com.meloda.fast.util.KeyboardUtils
|
||||
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
|
||||
@@ -77,7 +74,7 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
is ErrorEvent -> showErrorSnackbar(event.errorText)
|
||||
is CaptchaEvent -> showCaptchaDialog(event.sid, event.image)
|
||||
is ValidationEvent -> showValidationRequired(event.sid)
|
||||
is SuccessAuth -> goToMain(event)
|
||||
is SuccessAuth -> launchWebView()
|
||||
|
||||
is CodeSent -> showValidationDialog()
|
||||
is StartProgressEvent -> onProgressStarted()
|
||||
@@ -119,12 +116,8 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
parseAuthUrl(url)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
override fun onPageFinished(view: WebView, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
|
||||
val a = Jsoup.parse(url)
|
||||
|
||||
val b = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,15 +130,23 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
}
|
||||
|
||||
private fun launchWebView() {
|
||||
binding.webView.isVisible = true
|
||||
binding.webView.loadUrl(
|
||||
"https://oauth.vk.com/authorize?client_id=${UserConfig.FAST_APP_ID}&" +
|
||||
"display=mobile&scope=136297695&" +
|
||||
"access_token=${UserConfig.accessToken}&" +
|
||||
"sdk_package=com.meloda.fast.activity&" +
|
||||
"sdk_fingerprint=AA88DSADAS8DG8FSA8&" +
|
||||
"display=page&" +
|
||||
"revoke=1&" +
|
||||
"scope=136297695&" +
|
||||
"redirect_uri=${
|
||||
URLEncoder.encode(
|
||||
"https://oauth.vk.com/blank.html",
|
||||
Charsets.UTF_8.toString()
|
||||
)
|
||||
}&response_type=token&v=${VKConstants.API_VERSION}"
|
||||
}&" +
|
||||
"response_type=token&" +
|
||||
"v=${VKConstants.API_VERSION}"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -167,6 +168,8 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
val token = authData.first
|
||||
|
||||
UserConfig.fastToken = token
|
||||
|
||||
viewModel.openPrimaryScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,9 +208,9 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
else TextInputLayout.END_ICON_NONE
|
||||
}
|
||||
|
||||
binding.passwordInput.setOnEditorActionListener { _, _, event ->
|
||||
if (event == null) return@setOnEditorActionListener false
|
||||
return@setOnEditorActionListener if (event.action == EditorInfo.IME_ACTION_GO ||
|
||||
binding.passwordInput.setOnEditorActionListener edit@{ _, _, event ->
|
||||
if (event == null) return@edit false
|
||||
return@edit if (event.action == EditorInfo.IME_ACTION_GO ||
|
||||
(event.action == KeyEvent.ACTION_DOWN && (event.keyCode == KeyEvent.KEYCODE_ENTER || event.keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER))
|
||||
) {
|
||||
KeyboardUtils.hideKeyboardFrom(binding.passwordInput)
|
||||
@@ -237,7 +240,6 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
|
||||
KeyboardUtils.hideKeyboardFrom(requireView().findFocus())
|
||||
|
||||
|
||||
viewModel.login(
|
||||
login = loginString,
|
||||
password = passwordString
|
||||
@@ -383,16 +385,4 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
snackbar.animationMode = Snackbar.ANIMATION_MODE_FADE
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
private fun goToMain(event: SuccessAuth) = lifecycleScope.launch {
|
||||
UserConfig.userId = event.userId
|
||||
UserConfig.accessToken = event.vkToken
|
||||
|
||||
if (event.haveAuthorized) delay(500)
|
||||
|
||||
launchWebView()
|
||||
|
||||
findNavController().navigate(R.id.toMain)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +1,30 @@
|
||||
package com.meloda.fast.screens.login
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.terrakok.cicerone.Router
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.VKException
|
||||
import com.meloda.fast.api.network.auth.AuthDataSource
|
||||
import com.meloda.fast.api.network.auth.RequestAuthDirect
|
||||
import com.meloda.fast.base.viewmodel.*
|
||||
import com.meloda.fast.api.network.auth.AuthDirectRequest
|
||||
import com.meloda.fast.base.viewmodel.BaseViewModel
|
||||
import com.meloda.fast.base.viewmodel.ErrorEvent
|
||||
import com.meloda.fast.base.viewmodel.VkEvent
|
||||
import com.meloda.fast.common.Screens
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LoginViewModel @Inject constructor(
|
||||
private val dataSource: AuthDataSource
|
||||
private val dataSource: AuthDataSource,
|
||||
private val router: Router
|
||||
) : BaseViewModel() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LoginViewModel"
|
||||
}
|
||||
|
||||
fun login(
|
||||
login: String,
|
||||
password: String,
|
||||
@@ -24,7 +34,7 @@ class LoginViewModel @Inject constructor(
|
||||
makeJob(
|
||||
{
|
||||
dataSource.auth(
|
||||
RequestAuthDirect(
|
||||
AuthDirectRequest(
|
||||
grantType = VKConstants.Auth.GrantType.PASSWORD,
|
||||
clientId = VKConstants.VK_APP_ID,
|
||||
clientSecret = VKConstants.VK_SECRET,
|
||||
@@ -44,12 +54,24 @@ class LoginViewModel @Inject constructor(
|
||||
return@makeJob
|
||||
}
|
||||
|
||||
sendEvent(
|
||||
SuccessAuth(
|
||||
userId = it.userId,
|
||||
vkToken = it.accessToken
|
||||
)
|
||||
)
|
||||
UserConfig.userId = it.userId
|
||||
UserConfig.accessToken = it.accessToken
|
||||
|
||||
sendEvent(SuccessAuth())
|
||||
|
||||
// TODO: 19-Oct-21 do somewhen
|
||||
// makeJob({
|
||||
// dataSource.authWithApp(
|
||||
// AuthWithAppRequest(
|
||||
// accessToken = it.accessToken
|
||||
// )
|
||||
// )
|
||||
// }, onAnswer = { kindaAnswer ->
|
||||
// println("$TAG: AppAuthResponse: $kindaAnswer")
|
||||
// }
|
||||
// )
|
||||
|
||||
|
||||
},
|
||||
onError = {
|
||||
if (it !is VKException) {
|
||||
@@ -69,12 +91,14 @@ class LoginViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun openPrimaryScreen() {
|
||||
router.navigateTo(Screens.Conversations())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object CodeSent : VkEvent()
|
||||
|
||||
data class SuccessAuth(
|
||||
val haveAuthorized: Boolean = true,
|
||||
val userId: Int,
|
||||
val vkToken: String
|
||||
val haveAuthorized: Boolean = true
|
||||
) : VkEvent()
|
||||
@@ -1,45 +1,29 @@
|
||||
package com.meloda.fast.screens.main
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.viewbinding.library.fragment.viewBinding
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.base.BaseViewModelFragment
|
||||
import com.meloda.fast.databinding.FragmentMainBinding
|
||||
import com.meloda.fast.extensions.NavigationExtensions.setupWithNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainFragment : BaseViewModelFragment<MainViewModel>(R.layout.fragment_main) {
|
||||
class MainFragment : BaseViewModelFragment<MainViewModel>() {
|
||||
|
||||
override val viewModel: MainViewModel by viewModels()
|
||||
private val binding: FragmentMainBinding by viewBinding()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return View(context)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
if (!UserConfig.isLoggedIn()) findNavController().navigate(R.id.toLogin)
|
||||
else if (savedInstanceState == null) setupBottomBar()
|
||||
viewModel.checkSession(requireContext())
|
||||
}
|
||||
|
||||
private fun setupBottomBar() {
|
||||
val navGraphIds = listOf(
|
||||
R.navigation.messages,
|
||||
R.navigation.login
|
||||
)
|
||||
|
||||
with(binding.bottomBar) {
|
||||
selectedItemId = R.id.messages
|
||||
setupWithNavController(
|
||||
navGraphIds = navGraphIds,
|
||||
fragmentManager = childFragmentManager,
|
||||
containerId = R.id.fragmentContainer,
|
||||
intent = requireActivity().intent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,30 @@
|
||||
package com.meloda.fast.screens.main
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.github.terrakok.cicerone.Router
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.base.viewmodel.BaseViewModel
|
||||
import com.meloda.fast.common.Screens
|
||||
import com.meloda.fast.service.MessagesUpdateService
|
||||
import com.meloda.fast.service.OnlineService
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class MainViewModel : BaseViewModel()
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor(private val router: Router) : BaseViewModel() {
|
||||
|
||||
fun checkSession(context: Context) {
|
||||
if (UserConfig.isLoggedIn()) {
|
||||
router.navigateTo(Screens.Conversations())
|
||||
|
||||
context.run {
|
||||
startService(Intent(this, MessagesUpdateService::class.java))
|
||||
startService(Intent(this, OnlineService::class.java))
|
||||
}
|
||||
} else {
|
||||
router.navigateTo(Screens.Login())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +1,19 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.Space
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isNotEmpty
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.load
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import androidx.core.view.*
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.VkUtils
|
||||
@@ -27,8 +22,10 @@ import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.api.model.attachments.*
|
||||
import com.meloda.fast.databinding.*
|
||||
import com.meloda.fast.extensions.*
|
||||
import com.meloda.fast.extensions.ImageLoader.clear
|
||||
import com.meloda.fast.extensions.ImageLoader.loadWithGlide
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import com.meloda.fast.widget.RoundedFrameLayout
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
@@ -46,10 +43,18 @@ class AttachmentInflater constructor(
|
||||
|
||||
private val inflater = LayoutInflater.from(context)
|
||||
|
||||
private val playColor = ContextCompat.getColor(context, R.color.a3_700)
|
||||
private val playBackgroundColor = ContextCompat.getColor(context, R.color.a3_200)
|
||||
private val colorBackground = ContextCompat.getColor(
|
||||
context,
|
||||
R.color.colorBackground
|
||||
)
|
||||
private val colorSecondary = ContextCompat.getColor(
|
||||
context,
|
||||
R.color.colorSecondary
|
||||
)
|
||||
|
||||
var photoClickListener: ((url: String) -> Unit)? = null
|
||||
private var photoClickListener: ((url: String) -> Unit)? = null
|
||||
|
||||
private val displayMetrics get() = Resources.getSystem().displayMetrics
|
||||
|
||||
fun setPhotoClickListener(unit: ((url: String) -> Unit)?): AttachmentInflater {
|
||||
this.photoClickListener = unit
|
||||
@@ -57,15 +62,15 @@ class AttachmentInflater constructor(
|
||||
}
|
||||
|
||||
fun inflate() {
|
||||
if (message.attachments.isNullOrEmpty()) return
|
||||
attachments = message.attachments!!
|
||||
|
||||
container.removeAllViews()
|
||||
|
||||
if (textContainer.childCount > 1) {
|
||||
textContainer.removeViews(1, textContainer.childCount - 1)
|
||||
}
|
||||
|
||||
if (message.attachments.isNullOrEmpty()) return
|
||||
attachments = message.attachments!!
|
||||
|
||||
if (attachments.size == 1) {
|
||||
when (val attachment = attachments[0]) {
|
||||
is VkSticker -> return sticker(attachment)
|
||||
@@ -74,6 +79,7 @@ class AttachmentInflater constructor(
|
||||
is VkCall -> return call(attachment)
|
||||
is VkGraffiti -> return graffiti(attachment)
|
||||
is VkGift -> return gift(attachment)
|
||||
is VkStory -> return story(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,112 +119,107 @@ class AttachmentInflater constructor(
|
||||
}
|
||||
|
||||
private fun photo(photo: VkPhoto) {
|
||||
val size = photo.getSizeOrSmaller('y') ?: return
|
||||
val size = photo.getSizeOrSmaller(VkPhoto.SIZE_TYPE_807) ?: return
|
||||
|
||||
val newPhoto = ShapeableImageView(context).apply {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
// ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
size.width,
|
||||
size.height
|
||||
// AndroidUtils.px(size.width).roundToInt(),
|
||||
// AndroidUtils.px(size.height).roundToInt()
|
||||
)
|
||||
val specRatio = size.width.toFloat() / size.height.toFloat()
|
||||
val widthMultiplier: Float = when {
|
||||
specRatio > 1 -> 0.7F
|
||||
specRatio < 1 -> 0.45F
|
||||
else -> 0.35F
|
||||
}
|
||||
val ratio = "${size.width}:${size.height}"
|
||||
|
||||
shapeAppearanceModel =
|
||||
shapeAppearanceModel.withCornerSize {
|
||||
AndroidUtils.px(5)
|
||||
}
|
||||
|
||||
scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
val spacer = Space(context).apply {
|
||||
layoutParams =
|
||||
LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5.dpToPx())
|
||||
}
|
||||
|
||||
if (photoClickListener != null) {
|
||||
newPhoto.setOnClickListener { photoClickListener?.invoke(size.url) }
|
||||
} else {
|
||||
newPhoto.setOnClickListener(null)
|
||||
}
|
||||
|
||||
val spacer = Space(context).also {
|
||||
it.layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
AndroidUtils.px(5).roundToInt()
|
||||
)
|
||||
}
|
||||
|
||||
if (container.isNotEmpty())
|
||||
if (container.isNotEmpty()) {
|
||||
container.addView(spacer)
|
||||
}
|
||||
|
||||
if (attachments.size == 1) {
|
||||
val roundedLayout = RoundedFrameLayout(context).apply {
|
||||
setTopRightCornerRadius((if (message.isOut) 30 else 40).toFloat())
|
||||
setTopLeftCornerRadius((if (message.isOut) 40 else 30).toFloat())
|
||||
setBottomRightCornerRadius((if (message.isOut) 5 else 40).toFloat())
|
||||
setBottomLeftCornerRadius((if (message.isOut) 40 else 5).toFloat())
|
||||
val binding = ItemMessageAttachmentPhotoBinding.inflate(inflater, container, true)
|
||||
|
||||
val cornersRadius = 8.dpToPx().toFloat()
|
||||
|
||||
binding.border.run {
|
||||
shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius)
|
||||
|
||||
updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
width = (displayMetrics.widthPixels * widthMultiplier).roundToInt()
|
||||
dimensionRatio = ratio
|
||||
}
|
||||
loadWithGlide(
|
||||
drawable = ColorDrawable(colorSecondary),
|
||||
priority = Priority.IMMEDIATE,
|
||||
cacheStrategy = DiskCacheStrategy.NONE
|
||||
)
|
||||
}
|
||||
|
||||
binding.image.run {
|
||||
shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius * 0.8F)
|
||||
|
||||
if (photoClickListener != null) {
|
||||
setOnClickListener { photoClickListener?.invoke(size.url) }
|
||||
} else {
|
||||
setOnClickListener(null)
|
||||
}
|
||||
|
||||
roundedLayout.addView(newPhoto)
|
||||
container.addView(roundedLayout)
|
||||
} else {
|
||||
container.addView(newPhoto)
|
||||
loadWithGlide(
|
||||
url = size.url,
|
||||
crossFade = true,
|
||||
placeholderDrawable = ColorDrawable(colorBackground),
|
||||
priority = Priority.LOW
|
||||
)
|
||||
}
|
||||
|
||||
newPhoto.load(size.url) { crossfade(100) }
|
||||
}
|
||||
|
||||
private fun video(video: VkVideo) {
|
||||
val size = video.images[1]
|
||||
|
||||
val layout = FrameLayout(context).apply {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
val newPhoto = ShapeableImageView(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
AndroidUtils.px(size.width).roundToInt(),
|
||||
AndroidUtils.px(size.height).roundToInt()
|
||||
)
|
||||
|
||||
shapeAppearanceModel =
|
||||
shapeAppearanceModel.withCornerSize { AndroidUtils.px(5) }
|
||||
scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
}
|
||||
|
||||
val play = AppCompatImageView(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
AndroidUtils.px(50).roundToInt(),
|
||||
AndroidUtils.px(50).roundToInt()
|
||||
).also {
|
||||
it.gravity = Gravity.CENTER
|
||||
}
|
||||
|
||||
backgroundTintList = ColorStateList.valueOf(playBackgroundColor)
|
||||
imageTintList = ColorStateList.valueOf(playColor)
|
||||
|
||||
setBackgroundResource(R.drawable.ic_play_button_circle_background)
|
||||
setImageResource(R.drawable.ic_round_play_arrow_24)
|
||||
|
||||
setPadding(12)
|
||||
}
|
||||
|
||||
layout.addView(newPhoto)
|
||||
layout.addView(play)
|
||||
|
||||
val spacer = Space(context).apply {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
AndroidUtils.px(5).roundToInt()
|
||||
layoutParams =
|
||||
LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5.dpToPx())
|
||||
}
|
||||
if (container.isNotEmpty()) {
|
||||
container.addView(spacer)
|
||||
}
|
||||
|
||||
val size = video.imageForWidthAtLeast(300) ?: return
|
||||
val binding = ItemMessageAttachmentVideoBinding.inflate(inflater, container, true)
|
||||
|
||||
val specRatio = size.width.toFloat() / size.height.toFloat()
|
||||
val widthMultiplier: Float = when {
|
||||
specRatio > 1 -> 0.7F
|
||||
specRatio < 1 -> 0.45F
|
||||
else -> 0.35F
|
||||
}
|
||||
val ratio = "${size.width}:${size.height}"
|
||||
|
||||
val cornersRadius = 8.dpToPx().toFloat()
|
||||
|
||||
binding.border.run {
|
||||
shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius)
|
||||
|
||||
updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
width = (displayMetrics.widthPixels * widthMultiplier).roundToInt()
|
||||
dimensionRatio = ratio
|
||||
}
|
||||
loadWithGlide(
|
||||
drawable = ColorDrawable(colorSecondary),
|
||||
priority = Priority.IMMEDIATE,
|
||||
cacheStrategy = DiskCacheStrategy.NONE
|
||||
)
|
||||
}
|
||||
|
||||
if (container.isNotEmpty())
|
||||
container.addView(spacer)
|
||||
binding.image.run {
|
||||
shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius * 0.8F)
|
||||
|
||||
container.addView(layout)
|
||||
|
||||
newPhoto.load(size.url) { crossfade(100) }
|
||||
loadWithGlide(
|
||||
url = size.url,
|
||||
crossFade = true,
|
||||
placeholderDrawable = ColorDrawable(colorBackground),
|
||||
priority = Priority.LOW
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun audio(audio: VkAudio) {
|
||||
@@ -245,14 +246,14 @@ class AttachmentInflater constructor(
|
||||
val binding = ItemMessageAttachmentLinkBinding.inflate(inflater, textContainer, true)
|
||||
|
||||
binding.title.text = link.title
|
||||
binding.title.isVisible = !link.title.isNullOrBlank()
|
||||
binding.title.toggleVisibility(!link.title.isNullOrBlank())
|
||||
|
||||
binding.caption.text = link.caption
|
||||
binding.caption.isVisible = !link.caption.isNullOrBlank()
|
||||
binding.caption.toggleVisibility(!link.caption.isNullOrBlank())
|
||||
|
||||
link.photo?.getSizeOrSmaller('y')?.let {
|
||||
binding.preview.load(it.url) { crossfade(150) }
|
||||
binding.linkIcon.isVisible = false
|
||||
link.photo?.getSizeOrSmaller('y')?.let { size ->
|
||||
binding.preview.loadWithGlide(url = size.url, crossFade = true)
|
||||
binding.linkIcon.gone()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -264,7 +265,7 @@ class AttachmentInflater constructor(
|
||||
)
|
||||
)
|
||||
)
|
||||
binding.linkIcon.isVisible = true
|
||||
binding.linkIcon.visible()
|
||||
}
|
||||
|
||||
private fun sticker(sticker: VkSticker) {
|
||||
@@ -272,13 +273,12 @@ class AttachmentInflater constructor(
|
||||
|
||||
val url = sticker.urlForSize(352)
|
||||
|
||||
with(binding.image) {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
AndroidUtils.px(140).roundToInt(),
|
||||
AndroidUtils.px(140).roundToInt()
|
||||
)
|
||||
binding.image.run {
|
||||
val size = 140.dpToPx()
|
||||
|
||||
load(url) { crossfade(150) }
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(size, size)
|
||||
|
||||
loadWithGlide(url = url, crossFade = true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,14 +307,14 @@ class AttachmentInflater constructor(
|
||||
}
|
||||
|
||||
binding.postTitle.text = context.getString(postTitleRes)
|
||||
binding.postTitle.isVisible = false
|
||||
binding.postTitle.gone()
|
||||
|
||||
binding.avatar.isVisible = group != null || user != null
|
||||
binding.avatar.toggleVisibility(group != null || user != null)
|
||||
|
||||
if (binding.avatar.isVisible) {
|
||||
binding.avatar.load(avatar) { crossfade(150) }
|
||||
binding.avatar.loadWithGlide(url = avatar, crossFade = true)
|
||||
} else {
|
||||
binding.avatar.setImageDrawable(null)
|
||||
binding.avatar.clear()
|
||||
}
|
||||
|
||||
binding.title.text = title
|
||||
@@ -328,12 +328,13 @@ class AttachmentInflater constructor(
|
||||
private fun voice(voiceMessage: VkVoiceMessage) {
|
||||
val binding = ItemMessageAttachmentVoiceBinding.inflate(inflater, textContainer, true)
|
||||
|
||||
if (message.isOut)
|
||||
if (message.isOut) {
|
||||
val padding = 6.dpToPx()
|
||||
binding.root.updatePadding(
|
||||
bottom = AndroidUtils.px(6).roundToInt(),
|
||||
left = AndroidUtils.px(6).roundToInt()
|
||||
bottom = padding,
|
||||
left = padding
|
||||
)
|
||||
|
||||
}
|
||||
val waveform = IntArray(voiceMessage.waveform.size)
|
||||
voiceMessage.waveform.forEachIndexed { index, i -> waveform[index] = i }
|
||||
|
||||
@@ -352,8 +353,8 @@ class AttachmentInflater constructor(
|
||||
|
||||
if (message.isOut)
|
||||
binding.root.updatePadding(
|
||||
bottom = AndroidUtils.px(5).roundToInt(),
|
||||
left = AndroidUtils.px(6).roundToInt()
|
||||
bottom = 5.dpToPx(),
|
||||
left = 6.dpToPx()
|
||||
)
|
||||
|
||||
val callType =
|
||||
@@ -383,15 +384,17 @@ class AttachmentInflater constructor(
|
||||
|
||||
val url = graffiti.url
|
||||
|
||||
val heightCoefficient = graffiti.height / AndroidUtils.px(140)
|
||||
val size = 140.dpToPx()
|
||||
|
||||
with(binding.image) {
|
||||
val heightCoefficient = graffiti.height / size.toFloat()
|
||||
|
||||
binding.image.run {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
AndroidUtils.px(140).roundToInt(),
|
||||
size,
|
||||
(graffiti.height / heightCoefficient).roundToInt()
|
||||
)
|
||||
|
||||
load(url) { crossfade(150) }
|
||||
loadWithGlide(url = url, crossFade = true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,16 +403,72 @@ class AttachmentInflater constructor(
|
||||
|
||||
val url = gift.thumb256 ?: gift.thumb96 ?: gift.thumb48
|
||||
|
||||
with(binding.image) {
|
||||
shapeAppearanceModel = shapeAppearanceModel.withCornerSize { AndroidUtils.px(12) }
|
||||
binding.image.run {
|
||||
val size = 140.dpToPx()
|
||||
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
AndroidUtils.px(140).roundToInt(),
|
||||
AndroidUtils.px(140).roundToInt()
|
||||
)
|
||||
shapeAppearanceModel = shapeAppearanceModel.withCornerSize(12.dpToPx().toFloat())
|
||||
|
||||
load(url) { crossfade(150) }
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(size, size)
|
||||
|
||||
loadWithGlide(url = url, crossFade = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun story(story: VkStory) {
|
||||
val binding = ItemMessageAttachmentStoryBinding.inflate(inflater, container, true)
|
||||
|
||||
val photoUrl = story.photo?.getSizeOrSmaller(VkPhoto.SIZE_TYPE_807)?.url
|
||||
|
||||
val dimmerDrawable =
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_message_attachment_story_image_dimmer)
|
||||
|
||||
val cornersRadius = 24.dpToPx()
|
||||
|
||||
binding.caption.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
val margin = cornersRadius / 2
|
||||
updateMarginsRelative(
|
||||
top = margin,
|
||||
start = margin,
|
||||
end = margin,
|
||||
bottom = margin
|
||||
)
|
||||
}
|
||||
|
||||
binding.dimmer.loadWithGlide(
|
||||
drawable = dimmerDrawable,
|
||||
transformations = listOf(TypeTransformations.RoundedCornerCrop(cornersRadius)),
|
||||
priority = Priority.IMMEDIATE,
|
||||
cacheStrategy = DiskCacheStrategy.NONE
|
||||
)
|
||||
|
||||
binding.image.run {
|
||||
shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius.toFloat())
|
||||
|
||||
loadWithGlide(
|
||||
url = photoUrl,
|
||||
crossFade = true,
|
||||
placeholderDrawable = ColorDrawable(Color.GRAY)
|
||||
)
|
||||
}
|
||||
|
||||
if (story.ownerId == UserConfig.userId) {
|
||||
binding.caption.text = context.getString(R.string.message_attachment_story_your_story)
|
||||
} else {
|
||||
val storyOwnerUser = if (story.isFromUser()) profiles[story.ownerId] else null
|
||||
val storyOwnerGroup = if (story.isFromGroup()) groups[story.ownerId] else null
|
||||
|
||||
val ownerName = when {
|
||||
storyOwnerUser != null -> storyOwnerUser.fullName
|
||||
storyOwnerGroup != null -> storyOwnerGroup.name
|
||||
else -> null
|
||||
}
|
||||
|
||||
binding.caption.text = context.getString(
|
||||
R.string.message_attachment_story_story_from,
|
||||
ownerName
|
||||
)
|
||||
binding.caption.toggleVisibility(ownerName != null)
|
||||
binding.dimmer.toggleVisibility(binding.caption.isVisible)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
@@ -19,76 +21,89 @@ import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.api.model.attachments.VkPhoto
|
||||
import com.meloda.fast.base.adapter.BaseAdapter
|
||||
import com.meloda.fast.base.adapter.BaseHolder
|
||||
import com.meloda.fast.databinding.*
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
import com.meloda.fast.databinding.ItemMessageInBinding
|
||||
import com.meloda.fast.databinding.ItemMessageOutBinding
|
||||
import com.meloda.fast.databinding.ItemMessageServiceBinding
|
||||
import com.meloda.fast.extensions.dpToPx
|
||||
import com.meloda.fast.model.DataItem
|
||||
|
||||
class MessagesHistoryAdapter constructor(
|
||||
context: Context,
|
||||
values: MutableList<VkMessage>,
|
||||
val conversation: VkConversation,
|
||||
val profiles: HashMap<Int, VkUser> = hashMapOf(),
|
||||
val groups: HashMap<Int, VkGroup> = hashMapOf()
|
||||
) : BaseAdapter<VkMessage, MessagesHistoryAdapter.BasicHolder>(context, values, COMPARATOR) {
|
||||
) : BaseAdapter<DataItem<Int>, MessagesHistoryAdapter.BasicHolder>(context, Comparator) {
|
||||
|
||||
var avatarLongClickListener: ((position: Int) -> Unit)? = null
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
when {
|
||||
isPositionHeader(position) -> return HEADER
|
||||
isPositionFooter(position) -> return FOOTER
|
||||
return when (val item = getItem(position)) {
|
||||
is VkMessage -> {
|
||||
return when {
|
||||
item.action != null -> TypeService
|
||||
item.isOut -> TypeOutgoing
|
||||
!item.isOut -> TypeIncoming
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
is DataItem.Header -> {
|
||||
return TypeHeader
|
||||
}
|
||||
is DataItem.Footer -> {
|
||||
return TypeFooter
|
||||
}
|
||||
else -> -1
|
||||
}
|
||||
|
||||
getItem(position).let { message ->
|
||||
if (message.action != null) return SERVICE
|
||||
if (message.isOut) return OUTGOING
|
||||
if (!message.isOut) return INCOMING
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun isPositionHeader(position: Int) = position == 0
|
||||
private fun isPositionFooter(position: Int) = position >= actualSize
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicHolder {
|
||||
return when (viewType) {
|
||||
// magick numbers is great!
|
||||
HEADER -> Header(createEmptyView(60))
|
||||
FOOTER -> Footer(createEmptyView(36))
|
||||
SERVICE -> ServiceMessage(
|
||||
TypeHeader -> {
|
||||
Header(createEmptyView(60))
|
||||
}
|
||||
TypeFooter -> {
|
||||
Footer(
|
||||
createEmptyView(
|
||||
context.resources.getDimensionPixelSize(R.dimen.messages_history_input_panel_height_with_margins)
|
||||
)
|
||||
)
|
||||
}
|
||||
TypeService -> ServiceMessage(
|
||||
ItemMessageServiceBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
OUTGOING -> OutgoingMessage(
|
||||
TypeOutgoing -> OutgoingMessage(
|
||||
ItemMessageOutBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
INCOMING -> IncomingMessage(
|
||||
TypeIncoming -> IncomingMessage(
|
||||
ItemMessageInBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
else -> throw IllegalStateException("Wrong viewType: $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
// override fun initListeners(itemView: View, position: Int) {
|
||||
// if (itemView is AdapterView<*>) return
|
||||
//
|
||||
// itemView.setOnClickListener { onItemClickListener?.invoke(position, itemView) }
|
||||
// itemView.setOnLongClickListener { itemLongClickListener.invoke(position) }
|
||||
// }
|
||||
override fun onBindViewHolder(holder: BasicHolder, position: Int) {
|
||||
if (holder is Header || holder is Footer) {
|
||||
Log.d(
|
||||
"MessagesHistoryAdapter",
|
||||
"onBindViewHolder: index $position, holder is ${holder.javaClass.simpleName}. Skip"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(
|
||||
"MessagesHistoryAdapter",
|
||||
"onBindViewHolder: index $position, holder is ${holder.javaClass.simpleName}. Bind"
|
||||
)
|
||||
|
||||
val actualSize get() = values.size
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
if (actualSize == 0) return 2
|
||||
return super.getItemCount() + 2
|
||||
initListeners(holder.itemView, position)
|
||||
holder.bind(position)
|
||||
}
|
||||
|
||||
private fun createEmptyView(size: Int) = View(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
AndroidUtils.px(size).roundToInt()
|
||||
size
|
||||
)
|
||||
|
||||
isEnabled = false
|
||||
@@ -96,13 +111,6 @@ class MessagesHistoryAdapter constructor(
|
||||
isFocusable = false
|
||||
}
|
||||
|
||||
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 BasicHolder(v: View = View(context)) : BaseHolder(v)
|
||||
|
||||
inner class Header(v: View) : BasicHolder(v)
|
||||
@@ -114,10 +122,10 @@ class MessagesHistoryAdapter constructor(
|
||||
) : BasicHolder(binding.root) {
|
||||
|
||||
override fun bind(position: Int) {
|
||||
val message = getItem(position)
|
||||
val message = getItem(position) as VkMessage
|
||||
|
||||
val prevMessage = getOrNull(position - 1)
|
||||
val nextMessage = getOrNull(position + 1)
|
||||
val prevMessage = getVkMessage(getOrNull(position - 1))
|
||||
val nextMessage = getVkMessage(getOrNull(position + 1))
|
||||
|
||||
MessagesPreparator(
|
||||
context = context,
|
||||
@@ -159,9 +167,8 @@ class MessagesHistoryAdapter constructor(
|
||||
) : BasicHolder(binding.root) {
|
||||
|
||||
override fun bind(position: Int) {
|
||||
val message = getItem(position)
|
||||
|
||||
val prevMessage = getOrNull(position - 1)
|
||||
val message = getItem(position) as VkMessage
|
||||
val prevMessage = getVkMessage(getOrNull(position - 1))
|
||||
|
||||
MessagesPreparator(
|
||||
context = context,
|
||||
@@ -192,13 +199,12 @@ class MessagesHistoryAdapter constructor(
|
||||
private val youPrefix = context.getString(R.string.you_message_prefix)
|
||||
|
||||
init {
|
||||
binding.photo.shapeAppearanceModel.run {
|
||||
withCornerSize { AndroidUtils.px(4) }
|
||||
}
|
||||
binding.photo.shapeAppearanceModel =
|
||||
binding.photo.shapeAppearanceModel.withCornerSize(4.dpToPx().toFloat())
|
||||
}
|
||||
|
||||
override fun bind(position: Int) {
|
||||
val message = getItem(position)
|
||||
val message = getItem(position) as VkMessage
|
||||
|
||||
val messageUser =
|
||||
if (message.isUser()) profiles[message.fromId]
|
||||
@@ -241,59 +247,56 @@ class MessagesHistoryAdapter constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMessageById(id: Int): Int? {
|
||||
for (i in values.indices) {
|
||||
val message = values[i]
|
||||
if (message.id == id) {
|
||||
values.removeAt(i)
|
||||
return i
|
||||
}
|
||||
fun getVkMessage(item: DataItem<*>?): VkMessage? {
|
||||
if (item == null) return null
|
||||
if (item is VkMessage) return item
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun searchMessageIndex(messageId: Int): Int? {
|
||||
for (i in indices) {
|
||||
val message = getItem(i)
|
||||
if (message is VkMessage && message.id == messageId) return i
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun removeMessagesByIds(ids: List<Int>): List<Int> {
|
||||
val positions = mutableListOf<Int>()
|
||||
|
||||
for (i in values.indices) {
|
||||
val message = values[i]
|
||||
if (ids.contains(message.id)) {
|
||||
values.removeAt(i)
|
||||
positions += i
|
||||
}
|
||||
}
|
||||
|
||||
return positions
|
||||
}
|
||||
|
||||
fun searchMessageIndex(messageId: Int): Int? {
|
||||
for (i in values.indices) {
|
||||
val message = values[i]
|
||||
if (message.id == messageId) return i
|
||||
fun searchMessageById(messageId: Int): VkMessage? {
|
||||
for (i in indices) {
|
||||
val message = getItem(i)
|
||||
if (message is VkMessage && message.id == messageId) return message
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SERVICE = 1
|
||||
private const val HEADER = 0
|
||||
private const val FOOTER = 2
|
||||
private const val INCOMING = 3
|
||||
private const val OUTGOING = 4
|
||||
private const val TypeService = 1
|
||||
private const val TypeHeader = 0
|
||||
private const val TypeFooter = 2
|
||||
private const val TypeIncoming = 3
|
||||
private const val TypeOutgoing = 4
|
||||
|
||||
|
||||
private val COMPARATOR = object : DiffUtil.ItemCallback<VkMessage>() {
|
||||
private val Comparator = object : DiffUtil.ItemCallback<DataItem<Int>>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: VkMessage,
|
||||
newItem: VkMessage
|
||||
) = false
|
||||
oldItem: DataItem<Int>,
|
||||
newItem: DataItem<Int>
|
||||
): Boolean {
|
||||
return if (oldItem is VkMessage && newItem is VkMessage) {
|
||||
oldItem.id == newItem.id
|
||||
} else {
|
||||
oldItem is DataItem.Footer && newItem is DataItem.Footer
|
||||
|| oldItem is DataItem.Header && newItem is DataItem.Header
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("DiffUtilEquals")
|
||||
override fun areContentsTheSame(
|
||||
oldItem: VkMessage,
|
||||
newItem: VkMessage
|
||||
) = false
|
||||
oldItem: DataItem<Int>,
|
||||
newItem: DataItem<Int>
|
||||
): Boolean = oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.viewbinding.library.fragment.viewBinding
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@@ -32,7 +37,9 @@ import com.meloda.fast.base.viewmodel.StopProgressEvent
|
||||
import com.meloda.fast.base.viewmodel.VkEvent
|
||||
import com.meloda.fast.databinding.DialogMessageDeleteBinding
|
||||
import com.meloda.fast.databinding.FragmentMessagesHistoryBinding
|
||||
import com.meloda.fast.extensions.TextViewExtensions.clear
|
||||
import com.meloda.fast.extensions.*
|
||||
import com.meloda.fast.extensions.ImageLoader.clear
|
||||
import com.meloda.fast.extensions.ImageLoader.loadWithGlide
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import com.meloda.fast.util.TimeUtils
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -41,10 +48,26 @@ import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MessagesHistoryFragment :
|
||||
BaseViewModelFragment<MessagesHistoryViewModel>(R.layout.fragment_messages_history) {
|
||||
|
||||
companion object {
|
||||
const val ARG_USER: String = "user"
|
||||
const val ARG_GROUP: String = "group"
|
||||
const val ARG_CONVERSATION: String = "conversation"
|
||||
|
||||
private const val ATTACHMENT_PANEL_ANIMATION_DURATION = 150L
|
||||
|
||||
fun newInstance(bundle: Bundle): MessagesHistoryFragment {
|
||||
val fragment = MessagesHistoryFragment()
|
||||
fragment.arguments = bundle
|
||||
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
override val viewModel: MessagesHistoryViewModel by viewModels()
|
||||
private val binding: FragmentMessagesHistoryBinding by viewBinding()
|
||||
|
||||
@@ -55,21 +78,20 @@ class MessagesHistoryFragment :
|
||||
}
|
||||
|
||||
private val user: VkUser? by lazy {
|
||||
requireArguments().getParcelable("user")
|
||||
requireArguments().getParcelable(ARG_USER)
|
||||
}
|
||||
|
||||
private val group: VkGroup? by lazy {
|
||||
requireArguments().getParcelable("group")
|
||||
requireArguments().getParcelable(ARG_GROUP)
|
||||
}
|
||||
|
||||
private val conversation: VkConversation by lazy {
|
||||
requireNotNull(requireArguments().getParcelable("conversation"))
|
||||
requireNotNull(requireArguments().getParcelable(ARG_CONVERSATION))
|
||||
}
|
||||
|
||||
private val adapter: MessagesHistoryAdapter by lazy {
|
||||
MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also {
|
||||
MessagesHistoryAdapter(requireContext(), conversation).also {
|
||||
it.itemClickListener = this::onItemClick
|
||||
it.itemLongClickListener = this::onItemLongClick
|
||||
it.avatarLongClickListener = this::onAvatarLongClickListener
|
||||
}
|
||||
}
|
||||
@@ -90,6 +112,8 @@ class MessagesHistoryFragment :
|
||||
else -> null
|
||||
}
|
||||
|
||||
binding.back.setOnClickListener { requireActivity().onBackPressed() }
|
||||
|
||||
binding.title.ellipsize = TextUtils.TruncateAt.END
|
||||
binding.status.ellipsize = TextUtils.TruncateAt.END
|
||||
|
||||
@@ -121,7 +145,7 @@ class MessagesHistoryFragment :
|
||||
|
||||
binding.action.setOnClickListener { performAction() }
|
||||
|
||||
binding.recyclerView.addOnLayoutChangeListener { _, i, i2, i3, bottom, i5, i6, i7, oldBottom ->
|
||||
binding.recyclerView.addOnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
|
||||
if (bottom >= oldBottom) return@addOnLayoutChangeListener
|
||||
val lastVisiblePosition =
|
||||
(binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
|
||||
@@ -138,8 +162,8 @@ class MessagesHistoryFragment :
|
||||
val firstPosition =
|
||||
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||
|
||||
val message = adapter.getOrNull(firstPosition)
|
||||
message?.let {
|
||||
adapter.getOrNull(firstPosition)?.let {
|
||||
if (it !is VkMessage) return
|
||||
binding.timestamp.isVisible = true
|
||||
|
||||
val time = "${
|
||||
@@ -158,7 +182,7 @@ class MessagesHistoryFragment :
|
||||
|
||||
timestampTimer = Timer()
|
||||
timestampTimer?.schedule(2500) {
|
||||
recyclerView.post { binding.timestamp.isVisible = false }
|
||||
recyclerView.post { binding.timestamp.gone() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +209,8 @@ class MessagesHistoryFragment :
|
||||
.scaleY(1.25f)
|
||||
.setDuration(100)
|
||||
.withEndAction {
|
||||
if (getView() == null) return@withEndAction
|
||||
|
||||
binding.action.animate()
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
@@ -209,21 +235,37 @@ class MessagesHistoryFragment :
|
||||
}
|
||||
}
|
||||
|
||||
attachmentController.isPanelVisible.observe(viewLifecycleOwner) {
|
||||
if (it) binding.message.setSelection(binding.message.text.toString().length)
|
||||
attachmentController.isPanelVisible.observe(viewLifecycleOwner) { isVisible ->
|
||||
if (isVisible) binding.message.setSelection(binding.message.text.toString().length)
|
||||
|
||||
val layoutParams = binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams
|
||||
layoutParams.bottomMargin =
|
||||
if (it) (binding.attachmentPanel.height / 1.5).roundToInt() else 0
|
||||
val currentMargin =
|
||||
(binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin
|
||||
|
||||
val newMargin =
|
||||
if (isVisible) (binding.attachmentPanel.measuredHeight / 1.5).roundToInt()
|
||||
else 0
|
||||
|
||||
ValueAnimator.ofInt(currentMargin, newMargin).apply {
|
||||
duration = ATTACHMENT_PANEL_ANIMATION_DURATION
|
||||
interpolator = LinearInterpolator()
|
||||
|
||||
addUpdateListener { animator ->
|
||||
if (getView() == null) return@addUpdateListener
|
||||
val value = animator.animatedValue as Int
|
||||
binding.refreshLayout.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||
bottomMargin = value
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
binding.attachmentPanel.setOnClickListener c@{
|
||||
val message = attachmentController.message.value ?: return@c
|
||||
|
||||
val index = adapter.values.indexOf(message)
|
||||
val index = adapter.indexOf(message)
|
||||
if (index == -1) return@c
|
||||
|
||||
binding.recyclerView.smoothScrollToPosition(index)
|
||||
binding.recyclerView.scrollToPosition(index)
|
||||
}
|
||||
|
||||
binding.dismissReply.setOnClickListener {
|
||||
@@ -232,6 +274,11 @@ class MessagesHistoryFragment :
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
private fun getColor(@ColorRes resId: Int): Int {
|
||||
return ContextCompat.getColor(requireContext(), resId)
|
||||
}
|
||||
|
||||
private fun prepareAvatar() {
|
||||
val avatar = when {
|
||||
conversation.ownerId == VKConstants.FAST_GROUP_ID -> null
|
||||
@@ -241,46 +288,49 @@ class MessagesHistoryFragment :
|
||||
else -> null
|
||||
}
|
||||
|
||||
binding.avatar.isVisible = avatar != null
|
||||
val colorOnPrimary = getColor(R.color.colorOnPrimary)
|
||||
val colorUserAvatarAction = getColor(R.color.colorUserAvatarAction)
|
||||
val colorOnUserAvatarAction = getColor(R.color.colorOnUserAvatarAction)
|
||||
|
||||
val icLauncherColor = getColor(R.color.a1_500)
|
||||
|
||||
binding.avatar.toggleVisibility(avatar != null)
|
||||
|
||||
if (avatar == null) {
|
||||
binding.avatarPlaceholder.isVisible = true
|
||||
binding.avatarPlaceholder.visible()
|
||||
|
||||
if (conversation.ownerId == VKConstants.FAST_GROUP_ID) {
|
||||
binding.placeholderBack.setImageDrawable(
|
||||
ColorDrawable(
|
||||
ContextCompat.getColor(requireContext(), R.color.a1_400)
|
||||
)
|
||||
binding.placeholderBack.loadWithGlide(
|
||||
drawable = ColorDrawable(icLauncherColor),
|
||||
transformations = ImageLoader.userAvatarTransformations
|
||||
)
|
||||
binding.placeholder.imageTintList =
|
||||
ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.a1_0))
|
||||
ColorStateList.valueOf(colorOnPrimary)
|
||||
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.placeholderBack.loadWithGlide(
|
||||
drawable = ColorDrawable(colorOnUserAvatarAction),
|
||||
transformations = ImageLoader.userAvatarTransformations
|
||||
)
|
||||
binding.placeholder.imageTintList =
|
||||
ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.n2_500))
|
||||
ColorStateList.valueOf(colorUserAvatarAction)
|
||||
binding.placeholder.setImageResource(R.drawable.ic_account_circle_cut)
|
||||
binding.placeholder.setPadding(0)
|
||||
binding.avatar.setImageDrawable(null)
|
||||
binding.avatar.clear()
|
||||
}
|
||||
} else {
|
||||
binding.avatar.load(avatar) {
|
||||
crossfade(200)
|
||||
target {
|
||||
binding.avatarPlaceholder.isVisible = false
|
||||
binding.avatarPlaceholder.gone()
|
||||
binding.avatar.setImageDrawable(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.phantomIcon.isVisible = conversation.isPhantom
|
||||
binding.online.isVisible = user?.online == true
|
||||
binding.pin.isVisible = conversation.isPinned
|
||||
binding.phantomIcon.toggleVisibility(conversation.isPhantom)
|
||||
binding.online.toggleVisibility(user?.online)
|
||||
}
|
||||
|
||||
private fun performAction() {
|
||||
@@ -293,8 +343,10 @@ class MessagesHistoryFragment :
|
||||
|
||||
val date = System.currentTimeMillis()
|
||||
|
||||
val messageIndex = adapter.lastPosition
|
||||
|
||||
val message = VkMessage(
|
||||
id = -1,
|
||||
id = Int.MAX_VALUE,
|
||||
text = messageText,
|
||||
isOut = true,
|
||||
peerId = conversation.id,
|
||||
@@ -304,10 +356,10 @@ class MessagesHistoryFragment :
|
||||
replyMessage = attachmentController.message.value
|
||||
)
|
||||
|
||||
adapter.add(message)
|
||||
adapter.notifyItemInserted(adapter.actualSize - 1)
|
||||
binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
|
||||
binding.message.clear()
|
||||
adapter.add(message, beforeFooter = true, commitCallback = {
|
||||
binding.recyclerView.scrollToPosition(adapter.lastPosition)
|
||||
binding.message.clear()
|
||||
})
|
||||
|
||||
val replyMessage = attachmentController.message.value
|
||||
attachmentController.message.value = null
|
||||
@@ -316,8 +368,13 @@ class MessagesHistoryFragment :
|
||||
peerId = conversation.id,
|
||||
message = messageText,
|
||||
randomId = 0,
|
||||
replyTo = replyMessage?.id
|
||||
) { message.id = it }
|
||||
replyTo = replyMessage?.id,
|
||||
setId = { messageId ->
|
||||
val messageToUpdate = adapter[messageIndex] as VkMessage
|
||||
messageToUpdate.id = messageId
|
||||
adapter[messageIndex] = messageToUpdate
|
||||
}
|
||||
)
|
||||
}
|
||||
Action.EDIT -> {
|
||||
val message = attachmentController.message.value ?: return
|
||||
@@ -336,6 +393,7 @@ class MessagesHistoryFragment :
|
||||
Action.DELETE -> attachmentController.message.value?.let {
|
||||
showDeleteMessageDialog(it)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,12 +404,12 @@ class MessagesHistoryFragment :
|
||||
is StartProgressEvent -> onProgressStarted()
|
||||
is StopProgressEvent -> onProgressStopped()
|
||||
|
||||
is MessagesMarkAsImportant -> markMessagesAsImportant(event)
|
||||
is MessagesLoaded -> refreshMessages(event)
|
||||
is MessagesPin -> conversation.pinnedMessage = event.message
|
||||
is MessagesUnpin -> conversation.pinnedMessage = null
|
||||
is MessagesDelete -> deleteMessages(event)
|
||||
is MessagesEdit -> editMessage(event)
|
||||
is MessagesMarkAsImportantEvent -> markMessagesAsImportant(event)
|
||||
is MessagesLoadedEvent -> refreshMessages(event)
|
||||
is MessagesPinEvent -> conversation.pinnedMessage = event.message
|
||||
is MessagesUnpinEvent -> conversation.pinnedMessage = null
|
||||
is MessagesDeleteEvent -> deleteMessages(event)
|
||||
is MessagesEditEvent -> editMessage(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,26 +453,24 @@ class MessagesHistoryFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun markMessagesAsImportant(event: MessagesMarkAsImportant) {
|
||||
private fun markMessagesAsImportant(event: MessagesMarkAsImportantEvent) {
|
||||
var changed = false
|
||||
val positions = mutableListOf<Int>()
|
||||
|
||||
for (i in adapter.values.indices) {
|
||||
val message = adapter.values[i]
|
||||
for (i in adapter.indices) {
|
||||
val message = adapter[i] as VkMessage
|
||||
message.important = event.important
|
||||
if (event.messagesIds.contains(message.id)) {
|
||||
if (!changed) changed = true
|
||||
|
||||
positions.add(i)
|
||||
|
||||
adapter.values[i] = message
|
||||
adapter[i] = message
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) positions.forEach { adapter.notifyItemChanged(it) }
|
||||
}
|
||||
|
||||
private fun refreshMessages(event: MessagesLoaded) {
|
||||
private fun refreshMessages(event: MessagesLoadedEvent) {
|
||||
adapter.profiles += event.profiles
|
||||
adapter.groups += event.groups
|
||||
|
||||
@@ -424,22 +480,23 @@ class MessagesHistoryFragment :
|
||||
private fun fillRecyclerView(values: List<VkMessage>) {
|
||||
val smoothScroll = adapter.isNotEmpty()
|
||||
|
||||
adapter.values.clear()
|
||||
adapter.values += values.sortedBy { it.date }
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
|
||||
if (smoothScroll) binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
|
||||
else binding.recyclerView.scrollToPosition(adapter.lastPosition)
|
||||
adapter.setItems(
|
||||
values.sortedBy { it.date },
|
||||
withHeader = true,
|
||||
withFooter = true,
|
||||
commitCallback = {
|
||||
if (smoothScroll) binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
|
||||
else binding.recyclerView.scrollToPosition(adapter.lastPosition)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onItemClick(position: Int) {
|
||||
showOptionsDialog(position)
|
||||
}
|
||||
|
||||
private fun onItemLongClick(position: Int) = true
|
||||
|
||||
private fun onAvatarLongClickListener(position: Int) {
|
||||
val message = adapter.values[position]
|
||||
val message = adapter[position] as VkMessage
|
||||
|
||||
val messageUser = VkUtils.getMessageUser(message, adapter.profiles)
|
||||
val messageGroup = VkUtils.getMessageGroup(message, adapter.groups)
|
||||
@@ -449,7 +506,7 @@ class MessagesHistoryFragment :
|
||||
}
|
||||
|
||||
private fun showOptionsDialog(position: Int) {
|
||||
val message = adapter.values[position]
|
||||
val message = adapter[position] as VkMessage
|
||||
if (message.action != null) return
|
||||
|
||||
val time = getString(
|
||||
@@ -577,16 +634,14 @@ class MessagesHistoryFragment :
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun deleteMessages(event: MessagesDelete) {
|
||||
adapter.removeMessagesByIds(event.messagesIds).let {
|
||||
it.forEach { index -> adapter.notifyItemRemoved(index) }
|
||||
}
|
||||
private fun deleteMessages(event: MessagesDeleteEvent) {
|
||||
val messagesToDelete = event.messagesIds.mapNotNull { id -> adapter.searchMessageById(id) }
|
||||
adapter.removeAll(messagesToDelete)
|
||||
}
|
||||
|
||||
private fun editMessage(event: MessagesEdit) {
|
||||
private fun editMessage(event: MessagesEditEvent) {
|
||||
adapter.searchMessageIndex(event.message.id)?.let { index ->
|
||||
adapter.values[index] = event.message
|
||||
adapter.notifyItemChanged(index)
|
||||
adapter[index] = event.message
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,8 +665,6 @@ class MessagesHistoryFragment :
|
||||
}
|
||||
|
||||
private fun applyMessage(message: VkMessage) {
|
||||
showPanel()
|
||||
|
||||
val title = when {
|
||||
message.isGroup() && message.group.value != null -> message.group.value?.name
|
||||
message.isUser() && message.user.value != null -> message.user.value?.fullName
|
||||
@@ -637,6 +690,8 @@ class MessagesHistoryFragment :
|
||||
if (isEditing) {
|
||||
binding.message.setText(message.text)
|
||||
}
|
||||
|
||||
showPanel()
|
||||
}
|
||||
|
||||
private fun clearMessage() {
|
||||
@@ -651,28 +706,64 @@ class MessagesHistoryFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPanel(duration: Long = 250) {
|
||||
private fun showPanel() {
|
||||
binding.attachmentPanel.visible()
|
||||
binding.attachmentPanel.measure(
|
||||
View.MeasureSpec.AT_MOST, View.MeasureSpec.UNSPECIFIED
|
||||
)
|
||||
|
||||
if (attachmentController.isPanelVisible.value == false)
|
||||
attachmentController.isPanelVisible.value = true
|
||||
|
||||
val measuredHeight = binding.attachmentPanel.measuredHeight
|
||||
|
||||
binding.attachmentPanel.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||
height = 0
|
||||
}
|
||||
|
||||
binding.attachmentPanel.animate()
|
||||
.translationY(0f)
|
||||
.alpha(1f)
|
||||
.setDuration(duration)
|
||||
.withStartAction { binding.attachmentPanel.isVisible = true }
|
||||
.setDuration(ATTACHMENT_PANEL_ANIMATION_DURATION)
|
||||
.start()
|
||||
|
||||
ValueAnimator.ofInt(0, measuredHeight).apply {
|
||||
duration = ATTACHMENT_PANEL_ANIMATION_DURATION
|
||||
interpolator = LinearInterpolator()
|
||||
|
||||
addUpdateListener { animator ->
|
||||
if (view == null) return@addUpdateListener
|
||||
val value = animator.animatedValue as Int
|
||||
binding.attachmentPanel.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||
height = value
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun hidePanel(duration: Long = 250) {
|
||||
private fun hidePanel() {
|
||||
if (attachmentController.isPanelVisible.value == true)
|
||||
attachmentController.isPanelVisible.value = false
|
||||
|
||||
val currentHeight = binding.attachmentPanel.height
|
||||
|
||||
binding.attachmentPanel.animate()
|
||||
.alpha(0f)
|
||||
.translationY(50f)
|
||||
.setDuration(duration)
|
||||
.withEndAction { binding.attachmentPanel.isVisible = false }
|
||||
.translationY(75F)
|
||||
.setDuration(ATTACHMENT_PANEL_ANIMATION_DURATION)
|
||||
.start()
|
||||
|
||||
ValueAnimator.ofInt(currentHeight, 0).apply {
|
||||
duration = ATTACHMENT_PANEL_ANIMATION_DURATION
|
||||
interpolator = LinearInterpolator()
|
||||
|
||||
addUpdateListener { animator ->
|
||||
if (view == null) return@addUpdateListener
|
||||
val value = animator.animatedValue as Int
|
||||
|
||||
binding.attachmentPanel.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||
height = value
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.meloda.fast.api.LongPollEvent
|
||||
import com.meloda.fast.api.LongPollUpdatesParser
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.model.VkConversation
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
@@ -16,12 +18,25 @@ import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MessagesHistoryViewModel @Inject constructor(
|
||||
private val messages: MessagesDataSource
|
||||
private val messages: MessagesDataSource,
|
||||
updatesParser: LongPollUpdatesParser
|
||||
) : BaseViewModel() {
|
||||
|
||||
fun loadHistory(
|
||||
peerId: Int
|
||||
) = viewModelScope.launch {
|
||||
init {
|
||||
updatesParser.onNewMessage {
|
||||
// viewModelScope.launch { handleNewMessage(it) }
|
||||
}
|
||||
|
||||
updatesParser.onMessageEdited {
|
||||
viewModelScope.launch { handleEditedMessage(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleEditedMessage(event: LongPollEvent.VkMessageEditEvent) {
|
||||
sendEvent(com.meloda.fast.screens.messages.MessagesEditEvent(event.message))
|
||||
}
|
||||
|
||||
fun loadHistory(peerId: Int) = viewModelScope.launch {
|
||||
makeJob({
|
||||
messages.getHistory(
|
||||
MessagesGetHistoryRequest(
|
||||
@@ -66,7 +81,7 @@ class MessagesHistoryViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
sendEvent(
|
||||
MessagesLoaded(
|
||||
MessagesLoadedEvent(
|
||||
count = response.count,
|
||||
profiles = profiles,
|
||||
groups = groups,
|
||||
@@ -116,7 +131,7 @@ class MessagesHistoryViewModel @Inject constructor(
|
||||
onAnswer = {
|
||||
val response = it.response ?: return@makeJob
|
||||
sendEvent(
|
||||
MessagesMarkAsImportant(
|
||||
MessagesMarkAsImportantEvent(
|
||||
messagesIds = response,
|
||||
important = important
|
||||
)
|
||||
@@ -142,14 +157,14 @@ class MessagesHistoryViewModel @Inject constructor(
|
||||
},
|
||||
onAnswer = {
|
||||
val response = it.response ?: return@makeJob
|
||||
sendEvent(MessagesPin(response.asVkMessage()))
|
||||
sendEvent(MessagesPinEvent(response.asVkMessage()))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
makeJob({ messages.unpin(MessagesUnPinMessageRequest(peerId = peerId)) },
|
||||
onAnswer = {
|
||||
println("Fast::MessagesHistoryViewModel::unPin::Response::${it.response}")
|
||||
sendEvent(MessagesUnpin)
|
||||
sendEvent(MessagesUnpinEvent)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -172,7 +187,7 @@ class MessagesHistoryViewModel @Inject constructor(
|
||||
deleteForAll = deleteForAll
|
||||
)
|
||||
)
|
||||
}, onAnswer = { sendEvent(MessagesDelete(messagesIds = messagesIds ?: listOf())) })
|
||||
}, onAnswer = { sendEvent(MessagesDeleteEvent(messagesIds = messagesIds ?: emptyList())) })
|
||||
}
|
||||
|
||||
fun editMessage(
|
||||
@@ -195,13 +210,13 @@ class MessagesHistoryViewModel @Inject constructor(
|
||||
},
|
||||
onAnswer = {
|
||||
originalMessage.text = message
|
||||
sendEvent(MessagesEdit(originalMessage))
|
||||
sendEvent(com.meloda.fast.screens.messages.MessagesEditEvent(originalMessage))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class MessagesLoaded(
|
||||
data class MessagesLoadedEvent(
|
||||
val count: Int,
|
||||
val conversations: HashMap<Int, VkConversation>,
|
||||
val messages: List<VkMessage>,
|
||||
@@ -209,21 +224,12 @@ data class MessagesLoaded(
|
||||
val groups: HashMap<Int, VkGroup>
|
||||
) : VkEvent()
|
||||
|
||||
data class MessagesMarkAsImportant(
|
||||
val messagesIds: List<Int>,
|
||||
val important: Boolean
|
||||
) : VkEvent()
|
||||
data class MessagesMarkAsImportantEvent(val messagesIds: List<Int>, val important: Boolean) : VkEvent()
|
||||
|
||||
data class MessagesPin(
|
||||
val message: VkMessage
|
||||
) : VkEvent()
|
||||
data class MessagesPinEvent(val message: VkMessage) : VkEvent()
|
||||
|
||||
object MessagesUnpin : VkEvent()
|
||||
object MessagesUnpinEvent : VkEvent()
|
||||
|
||||
data class MessagesDelete(
|
||||
val messagesIds: List<Int>
|
||||
) : VkEvent()
|
||||
data class MessagesDeleteEvent(val messagesIds: List<Int>) : VkEvent()
|
||||
|
||||
data class MessagesEdit(
|
||||
val message: VkMessage
|
||||
) : VkEvent()
|
||||
data class MessagesEditEvent(val message: VkMessage) : VkEvent()
|
||||
@@ -12,6 +12,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import coil.load
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import com.meloda.fast.api.model.VkConversation
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
@@ -19,6 +20,9 @@ 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.extensions.gone
|
||||
import com.meloda.fast.extensions.toggleVisibility
|
||||
import com.meloda.fast.extensions.visible
|
||||
import com.meloda.fast.widget.BoundedLinearLayout
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -150,9 +154,7 @@ class MessagesPreparator constructor(
|
||||
}
|
||||
|
||||
private fun prepareUnreadIndicator() {
|
||||
if (unread != null) {
|
||||
unread.isVisible = message.isRead(conversation)
|
||||
}
|
||||
unread?.toggleVisibility(!message.isRead(conversation))
|
||||
}
|
||||
|
||||
private fun prepareSpacer() {
|
||||
@@ -160,12 +162,20 @@ class MessagesPreparator constructor(
|
||||
}
|
||||
|
||||
private fun prepareAttachments() {
|
||||
attachmentContainer?.removeAllViews()
|
||||
|
||||
textContainer?.let { textContainer ->
|
||||
if (textContainer.childCount > 1) {
|
||||
textContainer.removeViews(1, textContainer.childCount - 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (attachmentContainer != null && textContainer != null) {
|
||||
|
||||
if (message.attachments.isNullOrEmpty()) {
|
||||
attachmentContainer.isVisible = false
|
||||
attachmentContainer.removeAllViews()
|
||||
attachmentContainer.gone()
|
||||
} else {
|
||||
attachmentContainer.isVisible = true
|
||||
attachmentContainer.visible()
|
||||
|
||||
AttachmentInflater(
|
||||
context = context,
|
||||
@@ -208,11 +218,23 @@ class MessagesPreparator constructor(
|
||||
private fun prepareText() {
|
||||
if (bubble != null && text != null) {
|
||||
if (message.text == null) {
|
||||
text.isVisible = false
|
||||
bubble.isVisible = !message.attachments.isNullOrEmpty()
|
||||
text.gone()
|
||||
|
||||
val hasAttachments = !message.attachments.isNullOrEmpty()
|
||||
var shouldBeVisible = hasAttachments
|
||||
if (hasAttachments) {
|
||||
for (attachment in message.attachments ?: emptyList()) {
|
||||
if (VKConstants.separatedFromTextAttachments.contains(attachment.javaClass)) {
|
||||
shouldBeVisible = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bubble.toggleVisibility(shouldBeVisible)
|
||||
} else {
|
||||
text.isVisible = true
|
||||
bubble.isVisible = true
|
||||
text.visible()
|
||||
bubble.visible()
|
||||
text.text = VkUtils.prepareMessageText(message.text ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.meloda.fast.service
|
||||
|
||||
import android.util.Log
|
||||
import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest
|
||||
import com.meloda.fast.api.network.messages.MessagesDataSource
|
||||
import com.meloda.fast.api.network.longpoll.LongPollRepo
|
||||
import kotlinx.coroutines.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class LongPollService {
|
||||
}
|
||||
|
||||
class LongPollTask @Inject constructor(
|
||||
private val dataSource: MessagesDataSource,
|
||||
private val longPollRepo: LongPollRepo
|
||||
) : CoroutineScope {
|
||||
|
||||
companion object {
|
||||
const val TAG = "LongPollTask"
|
||||
}
|
||||
|
||||
private val job = SupervisorJob()
|
||||
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Log.d(TAG, "error: $throwable")
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Default + job + exceptionHandler
|
||||
|
||||
fun startPolling(): Job {
|
||||
if (job.isCompleted || job.isCancelled) throw Exception("Job is over")
|
||||
|
||||
return launch {
|
||||
val serverInfo = dataSource.getLongPollServer(
|
||||
MessagesGetLongPollServerRequest(
|
||||
needPts = true,
|
||||
version = 10
|
||||
)
|
||||
)
|
||||
|
||||
println("TESTJOPAAAAAA: $serverInfo")
|
||||
// val response = serverInfo.response ?: return@launch
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package com.meloda.fast.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.meloda.fast.api.LongPollUpdatesParser
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.VKException
|
||||
import com.meloda.fast.api.model.base.BaseVkLongPoll
|
||||
import com.meloda.fast.api.network.Answer
|
||||
import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest
|
||||
import com.meloda.fast.api.network.longpoll.LongPollRepo
|
||||
import com.meloda.fast.api.network.messages.MessagesDataSource
|
||||
import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MessagesUpdateService : Service(), CoroutineScope {
|
||||
|
||||
companion object {
|
||||
const val TAG = "LongPollTask"
|
||||
}
|
||||
|
||||
private val job = SupervisorJob()
|
||||
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Log.d(TAG, "error: $throwable")
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Default + job + exceptionHandler
|
||||
|
||||
@Inject
|
||||
lateinit var dataSource: MessagesDataSource
|
||||
|
||||
@Inject
|
||||
lateinit var longPollRepo: LongPollRepo
|
||||
|
||||
@Inject
|
||||
lateinit var updatesParser: LongPollUpdatesParser
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
launch { startPolling().join() }
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun startPolling(): Job {
|
||||
if (job.isCompleted || job.isCancelled) throw Exception("Job is over")
|
||||
|
||||
return launch {
|
||||
var serverInfo = getServerInfo()
|
||||
?: throw VKException(error = "bad VK response (server info)")
|
||||
|
||||
var lastUpdatesResponse: JsonObject? = getUpdatesResponse(serverInfo)
|
||||
?: throw VKException(error = "initiation error: bad VK response (last updates)")
|
||||
|
||||
var failCount = 0
|
||||
|
||||
while (job.isActive) {
|
||||
if (lastUpdatesResponse == null) {
|
||||
failCount++
|
||||
serverInfo = getServerInfo()
|
||||
?: throw VKException(error = "failed retrieving server info after error: bad VK response (server info #2)")
|
||||
lastUpdatesResponse = getUpdatesResponse(serverInfo)
|
||||
continue
|
||||
}
|
||||
|
||||
when (lastUpdatesResponse["failed"]?.asInt) {
|
||||
1 -> {
|
||||
var newTs = lastUpdatesResponse["ts"]?.asInt
|
||||
if (newTs == null) {
|
||||
newTs = serverInfo.ts
|
||||
failCount++
|
||||
}
|
||||
|
||||
lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs))
|
||||
}
|
||||
2, 3 -> {
|
||||
serverInfo = getServerInfo()
|
||||
?: throw VKException(
|
||||
error = "failed retrieving server info after error: bad VK response (server info #3)"
|
||||
)
|
||||
lastUpdatesResponse = getUpdatesResponse(serverInfo)
|
||||
}
|
||||
else -> {
|
||||
val newTs = lastUpdatesResponse["ts"]?.asInt
|
||||
|
||||
if (newTs == null) {
|
||||
failCount++
|
||||
} else {
|
||||
val updates = lastUpdatesResponse["updates"]?.asJsonArray
|
||||
|
||||
if (updates == null) {
|
||||
failCount++
|
||||
} else {
|
||||
updates.forEach { item ->
|
||||
item.asJsonArray?.also {
|
||||
launch {
|
||||
handleUpdateEvent(it)
|
||||
}
|
||||
} ?: failCount++
|
||||
}
|
||||
}
|
||||
|
||||
lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getServerInfo(): BaseVkLongPoll? {
|
||||
val response = dataSource.getLongPollServer(
|
||||
MessagesGetLongPollServerRequest(
|
||||
needPts = true,
|
||||
version = VKConstants.LP_VERSION
|
||||
)
|
||||
)
|
||||
|
||||
println("$TAG: serverInfoResponse: $response")
|
||||
|
||||
if (response is Answer.Error) return null
|
||||
if (response is Answer.Success) {
|
||||
return response.data.response
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun getUpdatesResponse(server: BaseVkLongPoll): JsonObject? {
|
||||
val response = dataSource.getLongPollUpdates(
|
||||
serverUrl = "https://${server.server}",
|
||||
params = LongPollGetUpdatesRequest(
|
||||
key = server.key,
|
||||
ts = server.ts,
|
||||
wait = 25,
|
||||
mode = 2 or 8 or 32 or 64 or 128
|
||||
)
|
||||
)
|
||||
|
||||
println("$TAG: lastUpdateResponse: $response")
|
||||
|
||||
if (response is Answer.Error) return null
|
||||
|
||||
if (response is Answer.Success) {
|
||||
return response.data
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun handleUpdateEvent(eventJson: JsonArray) {
|
||||
// println("$TAG: handleUpdateEvent: $eventJson")
|
||||
|
||||
updatesParser.parseNextUpdate(eventJson)
|
||||
}
|
||||
|
||||
// fun <T : Any> registerListener(eventType: Int, listener: VkEventCallback<T>) =
|
||||
// updatesParser.registerListener(eventType, listener)
|
||||
|
||||
override fun onDestroy() {
|
||||
try {
|
||||
job.cancel()
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
updatesParser.clearListeners()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.meloda.fast.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.network.account.AccountDataSource
|
||||
import com.meloda.fast.api.network.account.AccountSetOfflineRequest
|
||||
import com.meloda.fast.api.network.account.AccountSetOnlineRequest
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@AndroidEntryPoint
|
||||
class OnlineService : Service(), CoroutineScope {
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "OnlineService"
|
||||
}
|
||||
|
||||
private val job = SupervisorJob()
|
||||
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Log.d(MessagesUpdateService.TAG, "error: $throwable")
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Default + job + exceptionHandler
|
||||
|
||||
@Inject
|
||||
lateinit var dataSource: AccountDataSource
|
||||
|
||||
private var timer: Timer? = null
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
timer = Timer().apply {
|
||||
schedule(delay = 0, period = 60_000) {
|
||||
launch {
|
||||
setOffline()
|
||||
delay(5000)
|
||||
setOnline()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return START_STICKY_COMPATIBILITY
|
||||
}
|
||||
|
||||
private suspend fun setOnline() {
|
||||
println("$TAG: setOnline()")
|
||||
val response = dataSource.setOnline(
|
||||
AccountSetOnlineRequest(
|
||||
voip = false,
|
||||
accessToken = UserConfig.fastToken
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setOffline() {
|
||||
println("$TAG: setOffline()")
|
||||
val response = dataSource.setOffline(
|
||||
AccountSetOfflineRequest(
|
||||
accessToken = UserConfig.accessToken
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.net.NetworkCapabilities
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.TypedValue
|
||||
import androidx.annotation.AttrRes
|
||||
import com.meloda.fast.common.AppGlobal
|
||||
@@ -12,22 +11,8 @@ import com.meloda.fast.common.AppGlobal
|
||||
|
||||
object AndroidUtils {
|
||||
|
||||
fun px(dp: Float): Float {
|
||||
return dp * (AppGlobal.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
|
||||
}
|
||||
|
||||
fun px(dp: Int) = px(dp.toFloat())
|
||||
|
||||
fun dp(px: Float): Float {
|
||||
return px / (AppGlobal.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
|
||||
}
|
||||
|
||||
fun dp(px: Int) = dp(px.toFloat())
|
||||
|
||||
fun isDarkTheme(): Boolean {
|
||||
val currentNightMode =
|
||||
AppGlobal.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
return when (currentNightMode) {
|
||||
return when (AppGlobal.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
|
||||
Configuration.UI_MODE_NIGHT_YES -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ import androidx.annotation.StringRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.view.isVisible
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import kotlin.math.roundToInt
|
||||
import com.meloda.fast.extensions.dpToPx
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class NoItemsView @JvmOverloads constructor(
|
||||
@@ -43,7 +42,7 @@ class NoItemsView @JvmOverloads constructor(
|
||||
private fun create() {
|
||||
val a = context.obtainStyledAttributes(attrs, R.styleable.NoItemsView)
|
||||
|
||||
minimumWidth = AndroidUtils.px(256).roundToInt()
|
||||
minimumWidth = 256.dpToPx()
|
||||
minimumHeight = minimumWidth
|
||||
|
||||
orientation = VERTICAL
|
||||
@@ -51,9 +50,12 @@ class NoItemsView @JvmOverloads constructor(
|
||||
|
||||
noItemsPicture = ImageView(context)
|
||||
|
||||
val params = imageViewParams
|
||||
params.height = AndroidUtils.px(64).roundToInt()
|
||||
params.width = AndroidUtils.px(64).roundToInt()
|
||||
val imageViewSize = 64.dpToPx()
|
||||
|
||||
val params = imageViewParams.apply {
|
||||
height = imageViewSize
|
||||
width = imageViewSize
|
||||
}
|
||||
|
||||
noItemsPicture.layoutParams = params
|
||||
|
||||
@@ -72,10 +74,10 @@ class NoItemsView @JvmOverloads constructor(
|
||||
noItemsTextView = TextView(context)
|
||||
|
||||
val textParams = textViewParams
|
||||
textParams.width = AndroidUtils.px(256).roundToInt()
|
||||
textParams.width = 256.dpToPx()
|
||||
|
||||
if (noItemsDrawable != null) {
|
||||
textParams.topMargin = AndroidUtils.px(8).roundToInt()
|
||||
textParams.topMargin = 8.dpToPx()
|
||||
}
|
||||
|
||||
noItemsTextView.layoutParams = textParams
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<gradient
|
||||
android:angle="-90"
|
||||
android:endColor="@android:color/transparent"
|
||||
android:startColor="#BF000000" />
|
||||
|
||||
</shape>
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@color/messageOutStrokeColor" />
|
||||
android:color="?colorSurfaceVariant" />
|
||||
|
||||
<solid android:color="@color/messageOutColor" />
|
||||
<solid android:color="?colorSurface" />
|
||||
|
||||
<corners
|
||||
android:bottomLeftRadius="40dp"
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@color/messageOutStrokeColor" />
|
||||
android:color="?colorSurfaceVariant" />
|
||||
|
||||
<solid android:color="@color/messageOutColor" />
|
||||
<solid android:color="?colorSurface" />
|
||||
|
||||
<corners
|
||||
android:bottomLeftRadius="40dp"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/messageOutStrokeColor" />
|
||||
<solid android:color="?colorSurfaceVariant" />
|
||||
|
||||
<corners
|
||||
android:bottomLeftRadius="40dp"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/messageOutStrokeColor" />
|
||||
<solid android:color="?colorSurfaceVariant" />
|
||||
|
||||
<corners
|
||||
android:bottomLeftRadius="40dp"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<corners android:radius="50dp" />
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="?android:windowBackground" />
|
||||
android:color="?colorBackground" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,11H7.83l4.88,-4.88c0.39,-0.39 0.39,-1.03 0,-1.42 -0.39,-0.39 -1.02,-0.39 -1.41,0l-6.59,6.59c-0.39,0.39 -0.39,1.02 0,1.41l6.59,6.59c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L7.83,13H19c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1z" />
|
||||
</vector>
|
||||
@@ -1,21 +1,5 @@
|
||||
<?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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/main" />
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/root_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
@@ -1,84 +1,78 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout 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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/activity_horizontal_margin">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/captchaContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/captchaImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@drawable/ic_security"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/captchaLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/captchaInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/captcha_hint"
|
||||
android:imeOptions="actionGo"
|
||||
android:lines="1"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/activity_horizontal_margin">
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/captchaContainer"
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
android:orientation="horizontal">
|
||||
android:layout_marginEnd="@dimen/activity_horizontal_margin"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="@color/a1_600"
|
||||
android:text="@android:string/cancel"
|
||||
app:elevation="0dp" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/captchaImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@drawable/ic_security"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/captchaLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/captchaInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/captcha_hint"
|
||||
android:imeOptions="actionGo"
|
||||
android:lines="1"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ok"
|
||||
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/activity_horizontal_margin"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="@color/a1_600"
|
||||
android:text="@android:string/cancel"
|
||||
app:elevation="0dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ok"
|
||||
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="@color/a3_200"
|
||||
android:text="@android:string/ok"
|
||||
app:elevation="0dp" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="@color/a3_200"
|
||||
android:text="@android:string/ok"
|
||||
app:elevation="0dp" />
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
</LinearLayout>
|
||||
@@ -1,22 +1,17 @@
|
||||
<?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">
|
||||
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="12dp">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:id="@+id/check"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="12dp">
|
||||
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:id="@+id/check"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:text="@string/message_delete_for_all"
|
||||
app:useMaterialThemeColors="true" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</layout>
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:text="@string/message_delete_for_all"
|
||||
app:useMaterialThemeColors="true" />
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
@@ -1,77 +1,71 @@
|
||||
<?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">
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/activity_horizontal_margin">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/codeContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/codeImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@drawable/ic_security"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/codeLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/codeInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/code_hint"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="number"
|
||||
android:lines="1"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/activity_horizontal_margin">
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/codeContainer"
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
android:orientation="horizontal">
|
||||
android:layout_marginEnd="@dimen/activity_horizontal_margin"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="@color/n1_900"
|
||||
android:text="@android:string/cancel"
|
||||
app:elevation="0dp" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/codeImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@drawable/ic_security"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/codeLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/codeInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/code_hint"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="number"
|
||||
android:lines="1"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ok"
|
||||
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/activity_horizontal_margin"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="@color/n1_900"
|
||||
android:text="@android:string/cancel"
|
||||
app:elevation="0dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ok"
|
||||
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="@color/a3_200"
|
||||
android:text="@android:string/ok"
|
||||
app:elevation="0dp" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="@color/a3_200"
|
||||
android:text="@android:string/ok"
|
||||
app:elevation="0dp" />
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
</LinearLayout>
|
||||
@@ -1,142 +1,92 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="wrap_content"
|
||||
app:elevation="0dp">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBar"
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:elevation="0dp">
|
||||
android:layout_height="?actionBarSize"
|
||||
android:background="?colorBackground"
|
||||
android:elevation="0dp"
|
||||
app:layout_scrollFlags="scroll|enterAlways|snap"
|
||||
app:menu="@menu/fragment_conversations"
|
||||
app:title="@string/title_messages"
|
||||
app:titleTextColor="?colorOnBackground">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/collapsingToolbarLayout"
|
||||
android:layout_width="match_parent"
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/avatarContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="0dp"
|
||||
app:collapsedTitleTextAppearance="@style/CollapsingToolbarCollapsedTitle"
|
||||
app:expandedTitleTextAppearance="@style/CollapsingToolbarTitle"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
|
||||
app:title="Messages">
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_collapseMode="none">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/expandedImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="140dp"
|
||||
android:elevation="0dp" />
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/search"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_search"
|
||||
android:tint="?colorPrimary" />
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/avatarContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:paddingBottom="30dp"
|
||||
app:layout_collapseMode="none">
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/search"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_search"
|
||||
android:tint="?colorSecondary3Variant" />
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
</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="none"
|
||||
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"-->
|
||||
<!-- android:visibility="gone"-->
|
||||
<!-- 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.AppBarLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refreshLayout"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_conversation" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_conversation" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/createChat"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_baseline_create_24"
|
||||
app:elevation="3dp"
|
||||
app:fabSize="normal"
|
||||
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
||||
app:pressedTranslationZ="1dp"
|
||||
app:shapeAppearanceOverlay="@style/RoundedView.56"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/createChat"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_baseline_create_24"
|
||||
app:elevation="3dp"
|
||||
app:fabSize="normal"
|
||||
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
||||
app:pressedTranslationZ="1dp"
|
||||
app:shapeAppearanceOverlay="@style/RoundedView.56"
|
||||
tools:ignore="ContentDescription" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,148 +1,141 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.appcompat.widget.LinearLayoutCompat 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"
|
||||
android:id="@+id/loginRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/loginRoot"
|
||||
<WebView
|
||||
android:id="@+id/webView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
android:visibility="gone" />
|
||||
|
||||
<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"
|
||||
android:fillViewport="true">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/logoContainer"
|
||||
android:layout_width="140dp"
|
||||
android:layout_height="140dp"
|
||||
android:layout_gravity="center_horizontal">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/logoImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:padding="42dp"
|
||||
android:src="@drawable/ic_fast_lightning"
|
||||
app:tint="?colorAccent" />
|
||||
</FrameLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/loginContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
android:layout_marginTop="48dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/logoContainer"
|
||||
android:layout_width="140dp"
|
||||
android:layout_height="140dp"
|
||||
android:layout_gravity="center_horizontal">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/logoImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:padding="42dp"
|
||||
android:src="@drawable/ic_fast_lightning"
|
||||
app:tint="?colorAccent" />
|
||||
</FrameLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/loginImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@drawable/ic_baseline_account_circle_24"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/loginContainer"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/loginLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="48dp"
|
||||
android:orientation="horizontal">
|
||||
app:boxStrokeErrorColor="@android:color/transparent">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/loginImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@drawable/ic_baseline_account_circle_24"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/loginLayout"
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/loginInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:boxStrokeErrorColor="@android:color/transparent">
|
||||
android:layout_height="48dp"
|
||||
android:hint="@string/login_hint"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="textEmailAddress" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/loginInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:hint="@string/login_hint"
|
||||
android:imeOptions="actionGo"
|
||||
android:inputType="textEmailAddress" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/passwordContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/passwordImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@drawable/ic_key"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/passwordLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:boxStrokeErrorColor="@android:color/transparent"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:passwordToggleTint="?colorAccent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/passwordInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:fontFamily="@font/roboto_regular"
|
||||
android:hint="@string/password_login_hint"
|
||||
android:imeOptions="actionGo"
|
||||
android:typeface="normal" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/auth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:layout_marginTop="12dp"
|
||||
android:backgroundTint="@color/a1_600"
|
||||
android:fontFamily="@font/google_sans_medium"
|
||||
android:letterSpacing="0"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/log_in"
|
||||
android:textSize="14sp"
|
||||
app:cornerRadius="50dp"
|
||||
app:elevation="16dp"
|
||||
app:icon="@drawable/ic_arrow_end"
|
||||
app:iconGravity="end" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</layout>
|
||||
<LinearLayout
|
||||
android:id="@+id/passwordContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/passwordImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@drawable/ic_key"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/passwordLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:boxStrokeErrorColor="@android:color/transparent"
|
||||
app:passwordToggleEnabled="true"
|
||||
app:passwordToggleTint="?colorAccent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/passwordInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:fontFamily="@font/roboto_regular"
|
||||
android:hint="@string/password_login_hint"
|
||||
android:imeOptions="actionGo"
|
||||
android:typeface="normal" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/auth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:layout_marginTop="12dp"
|
||||
android:backgroundTint="@color/a1_600"
|
||||
android:fontFamily="@font/google_sans_medium"
|
||||
android:letterSpacing="0"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/log_in"
|
||||
android:textSize="14sp"
|
||||
app:cornerRadius="50dp"
|
||||
app:elevation="16dp"
|
||||
app:icon="@drawable/ic_arrow_end"
|
||||
app:iconGravity="end" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
@@ -1,27 +0,0 @@
|
||||
<?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">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottomBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="?colorSurface"
|
||||
app:elevation="0.5dp"
|
||||
app:labelVisibilityMode="unlabeled"
|
||||
app:menu="@menu/activity_main_bottom" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</layout>
|
||||
@@ -1,339 +1,328 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="86dp">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refreshLayout"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="86dp">
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="100"
|
||||
tools:listitem="@layout/item_message_out" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="100"
|
||||
tools:listitem="@layout/item_message_out" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
<FrameLayout
|
||||
android:id="@+id/toolbarContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:background="@drawable/ic_messages_history_toolbar_gradient_background"
|
||||
android:backgroundTint="?colorBackground"
|
||||
android:minHeight="140dp">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/toolbarContainer"
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:background="@drawable/ic_messages_history_toolbar_gradient_background"
|
||||
android:backgroundTint="@color/n1_50"
|
||||
android:minHeight="140dp">
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="18dp"
|
||||
android:paddingEnd="30dp"
|
||||
android:paddingBottom="24dp">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="30dp"
|
||||
android:paddingTop="18dp"
|
||||
android:paddingBottom="24dp">
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/back"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_round_arrow_back_24"
|
||||
android:tint="?colorOnBackground" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp">
|
||||
android:id="@+id/avatarPlaceholder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/avatar"
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/placeholderBack"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
android:layout_margin="1dp"
|
||||
tools:src="@color/colorOnUserAvatarAction" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/avatarPlaceholder"
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/placeholder"
|
||||
android:layout_width="match_parent"
|
||||
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"
|
||||
app:tint="@color/n2_500" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/online"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@color/n1_50" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_online_pc"
|
||||
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
|
||||
android:id="@+id/service"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="end|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/phantomIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:elevation="1dp"
|
||||
android:src="@drawable/ic_phantom"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/n2_10"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/callIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:elevation="1dp"
|
||||
android:src="@drawable/ic_attachment_group_call"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/n2_0" />
|
||||
|
||||
</FrameLayout>
|
||||
android:layout_height="match_parent"
|
||||
android:src="@drawable/ic_account_circle_cut"
|
||||
app:tint="@color/colorUserAvatarAction" />
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:orientation="vertical">
|
||||
<FrameLayout
|
||||
android:id="@+id/online"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/n1_900"
|
||||
android:textSize="24sp"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/online_border"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:src="?colorBackground" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.7"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/n1_900"
|
||||
tools:text="Online" />
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="10dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_online_pc"
|
||||
android:tint="?colorPrimaryVariant" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
<FrameLayout
|
||||
android:id="@+id/service"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_gravity="end|top"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:layout_marginBottom="30dp"
|
||||
android:elevation="2dp"
|
||||
android:enabled="false"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="4dp"
|
||||
android:textColor="@color/n1_900"
|
||||
android:visibility="gone"
|
||||
app:chipBackgroundColor="@color/n1_100"
|
||||
app:textEndPadding="12dp"
|
||||
app:textStartPadding="12dp"
|
||||
tools:text="today"
|
||||
tools:visibility="visible" />
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/ic_back"
|
||||
android:backgroundTint="@color/colorUserAvatarAction"
|
||||
android:elevation="0.5dp" />
|
||||
|
||||
</FrameLayout>
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/phantomIcon"
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="10dp"
|
||||
android:layout_gravity="center"
|
||||
android:elevation="1dp"
|
||||
android:src="@drawable/ic_phantom"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/colorOnUserAvatarAction"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<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"
|
||||
android:visibility="gone"
|
||||
app:layout_anchor="@+id/messagePanel"
|
||||
app:layout_anchorGravity="center_vertical|top"
|
||||
tools:visibility="visible">
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/callIcon"
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="10dp"
|
||||
android:layout_gravity="center"
|
||||
android:elevation="1dp"
|
||||
android:src="@drawable/ic_attachment_group_call"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/colorOnUserAvatarAction"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/replyMessage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
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="?selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_round_close_20"
|
||||
android:tint="@color/n1_800" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/replyMessageText"
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/google_sans_regular"
|
||||
android:maxLines="1"
|
||||
android:textColor="?textColorPrimary"
|
||||
android:textSize="16sp"
|
||||
app:fontFamily="@font/roboto_regular"
|
||||
tools:text="Short Message." />
|
||||
android:textColor="?colorOnBackground"
|
||||
android:textSize="20sp"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.7"
|
||||
android:maxLines="1"
|
||||
android:textColor="?colorOnBackground"
|
||||
tools:text="Online" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/ic_message_panel_gradient"
|
||||
android:backgroundTint="@color/n1_50" />
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:elevation="2dp"
|
||||
android:enabled="false"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="4dp"
|
||||
android:textColor="?colorOnBackground"
|
||||
android:visibility="gone"
|
||||
app:chipBackgroundColor="?colorBackgroundVariant"
|
||||
app:chipCornerRadius="16dp"
|
||||
app:chipStrokeWidth="0dp"
|
||||
app:textEndPadding="12dp"
|
||||
app:textStartPadding="12dp"
|
||||
tools:text="today"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</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="?colorSurfaceVariant"
|
||||
android:minHeight="105dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:translationY="50dp"
|
||||
android:visibility="gone"
|
||||
app:layout_anchor="@+id/messagePanel"
|
||||
app:layout_anchorGravity="center_vertical|top"
|
||||
tools:translationY="0dp"
|
||||
tools:visibility="gone">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/messagePanel"
|
||||
android:id="@+id/replyMessage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_margin="12dp"
|
||||
android:background="@drawable/ic_message_panel_background"
|
||||
android:backgroundTint="?colorSurface"
|
||||
android:clickable="true"
|
||||
android:elevation="3dp"
|
||||
android:focusable="true"
|
||||
android:minHeight="60dp"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="@string/message_input_hint"
|
||||
android:singleLine="true" />
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/attach"
|
||||
<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="?colorOnBackground"
|
||||
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="?selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_round_close_20"
|
||||
android:tint="?colorOnBackground" />
|
||||
|
||||
</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:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_baseline_attach_file_24"
|
||||
android:tint="?colorSecondary3" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_round_mic_24"
|
||||
android:tint="?colorSecondary3" />
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?colorOnBackground"
|
||||
android:textSize="16sp"
|
||||
app:fontFamily="@font/roboto_regular"
|
||||
tools:text="Short Message." />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/ic_message_panel_gradient"
|
||||
android:backgroundTint="?colorBackground" />
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/messagePanel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_margin="12dp"
|
||||
android:background="@drawable/ic_message_panel_background"
|
||||
android:backgroundTint="@color/colorSurface"
|
||||
android:clickable="true"
|
||||
android:elevation="3dp"
|
||||
android:focusable="true"
|
||||
android:minHeight="60dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:gravity="center_vertical"
|
||||
android:hint="@string/message_input_hint"
|
||||
android:maxLines="3"
|
||||
android:textColor="?colorOnBackground"
|
||||
android:textColorHint="@color/colorOnBackground50" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/attach"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_baseline_attach_file_24"
|
||||
android:tint="?colorPrimary" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_round_mic_24"
|
||||
android:tint="?colorPrimary" />
|
||||
|
||||
</layout>
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,255 +1,253 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout 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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="4dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="4dp"
|
||||
android:orientation="horizontal">
|
||||
android:layout_marginStart="20dp"
|
||||
android:backgroundTint="?colorBackgroundVariant"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="32dp"
|
||||
tools:background="@drawable/ic_message_unread">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:backgroundTint="@color/n1_100"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="32dp"
|
||||
tools:background="@drawable/ic_message_unread">
|
||||
<FrameLayout
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp">
|
||||
android:id="@+id/avatarPlaceholder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/avatar"
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/placeholderBack"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
android:layout_margin="1dp"
|
||||
tools:src="@color/colorOnUserAvatarAction" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/avatarPlaceholder"
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/placeholder"
|
||||
android:layout_width="match_parent"
|
||||
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"
|
||||
app:tint="@color/n2_500" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/online"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@color/n1_50" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_online_pc"
|
||||
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
|
||||
android:id="@+id/service"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="end|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/phantomIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:elevation="1dp"
|
||||
android:src="@drawable/ic_phantom"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/n2_10"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/callIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:elevation="1dp"
|
||||
android:src="@drawable/ic_attachment_group_call"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/n2_0" />
|
||||
|
||||
</FrameLayout>
|
||||
android:layout_height="match_parent"
|
||||
android:src="@drawable/ic_account_circle_cut"
|
||||
app:tint="@color/colorUserAvatarAction" />
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<FrameLayout
|
||||
android:id="@+id/online"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/online_border"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="?colorBackground" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_online_pc"
|
||||
android:tint="?colorPrimaryVariant" />
|
||||
|
||||
</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/colorUserAvatarAction"
|
||||
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/colorOnUserAvatarAction" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/service"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="end|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/colorUserAvatarAction"
|
||||
android:elevation="0.5dp" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/phantomIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:elevation="1dp"
|
||||
android:src="@drawable/ic_phantom"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/colorOnUserAvatarAction"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/callIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:elevation="1dp"
|
||||
android:src="@drawable/ic_attachment_group_call"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/colorOnUserAvatarAction"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toStartOf="@+id/date"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:fontFamily="@font/google_sans_regular"
|
||||
android:maxLines="2"
|
||||
android:textColor="?textColorPrimary"
|
||||
android:textSize="22sp"
|
||||
tools:text="Title" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/counter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_weight="0"
|
||||
android:background="@drawable/ic_back"
|
||||
android:backgroundTint="?colorSecondary3"
|
||||
android:gravity="center"
|
||||
android:minWidth="18dp"
|
||||
android:textColor="?colorOnSecondary3"
|
||||
android:textSize="10sp"
|
||||
android:visibility="gone"
|
||||
tools:text="12"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:alpha="0.5"
|
||||
android:fontFamily="@font/roboto_regular"
|
||||
android:gravity="end|center_vertical"
|
||||
android:textColor="?textColorSecondaryVariant"
|
||||
tools:text="20:00" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toStartOf="@+id/date"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/textAttachment"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:src="@drawable/ic_baseline_attach_file_24"
|
||||
android:visibility="gone"
|
||||
app:tint="?textColorSecondaryVariant"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/message"
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.7"
|
||||
android:fontFamily="@font/roboto_regular"
|
||||
android:layout_weight="1"
|
||||
android:fontFamily="@font/google_sans_regular"
|
||||
android:maxLines="2"
|
||||
android:textColor="?textColorPrimary"
|
||||
android:textSize="16sp"
|
||||
tools:text="Message" />
|
||||
android:textColor="?colorOnBackground"
|
||||
android:textSize="20sp"
|
||||
tools:text="Title" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/counter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_weight="0"
|
||||
android:background="@drawable/ic_back"
|
||||
android:backgroundTint="?colorOnBackgroundVariantContainer"
|
||||
android:gravity="center"
|
||||
android:minWidth="18dp"
|
||||
android:paddingHorizontal="2dp"
|
||||
android:textColor="?colorOnBackgroundVariantOnContainer"
|
||||
android:textSize="11sp"
|
||||
android:visibility="gone"
|
||||
tools:text="12"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:alpha="0.5"
|
||||
android:fontFamily="@font/roboto_regular"
|
||||
android:gravity="end|center_vertical"
|
||||
android:textColor="?colorOutline"
|
||||
tools:text="20:00" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/textAttachment"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:src="@drawable/ic_baseline_attach_file_24"
|
||||
android:visibility="gone"
|
||||
app:tint="?colorOutline"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.7"
|
||||
android:fontFamily="@font/roboto_regular"
|
||||
android:maxLines="2"
|
||||
android:textColor="?colorOnBackground"
|
||||
android:textSize="16sp"
|
||||
tools:text="Message" />
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?selectableItemBackground" />
|
||||
</FrameLayout>
|
||||
|
||||
</layout>
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?selectableItemBackground"
|
||||
tools:visibility="gone" />
|
||||
</FrameLayout>
|
||||
@@ -1,59 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.appcompat.widget.LinearLayoutCompat 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"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="@drawable/ic_play_button_circle_background"
|
||||
android:backgroundTint="?colorPrimaryVariant">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_round_play_arrow_24"
|
||||
app:tint="@color/a3_700" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
android:layout_marginStart="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="@drawable/ic_play_button_circle_background"
|
||||
android:backgroundTint="@color/a3_200">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_round_play_arrow_24"
|
||||
app:tint="@color/a3_700" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/google_sans_regular"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/n1_800"
|
||||
android:textSize="18sp"
|
||||
tools:text="Даня, дай Фаст" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/artist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.8"
|
||||
android:fontFamily="@font/roboto_regular"
|
||||
android:textColor="@color/n1_800"
|
||||
tools:text="Эльчин Оруджев | 0:36" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/google_sans_regular"
|
||||
android:maxLines="1"
|
||||
android:textColor="?colorOnBackground"
|
||||
android:textSize="18sp"
|
||||
tools:text="Даня, дай Фаст" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/artist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.8"
|
||||
android:fontFamily="@font/roboto_regular"
|
||||
android:textColor="?colorOnBackground"
|
||||
tools:text="Эльчин Оруджев | 0:36" />
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</layout>
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
@@ -1,59 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.appcompat.widget.LinearLayoutCompat 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"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="@drawable/ic_play_button_circle_background"
|
||||
android:backgroundTint="?colorPrimaryVariant">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_attachment_call"
|
||||
app:tint="@color/a3_700" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="@drawable/ic_play_button_circle_background"
|
||||
android:backgroundTint="@color/a3_200">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_attachment_call"
|
||||
app:tint="@color/a3_700" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:orientation="vertical">
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/google_sans_regular"
|
||||
android:maxLines="1"
|
||||
android:textColor="?colorOnBackground"
|
||||
android:textSize="18sp"
|
||||
tools:text="Исходящий звонок" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/google_sans_regular"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/n1_800"
|
||||
android:textSize="18sp"
|
||||
tools:text="Исходящий звонок" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/state"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.8"
|
||||
android:fontFamily="@font/roboto_regular"
|
||||
android:textColor="@color/n1_800"
|
||||
tools:text="Отменён" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/state"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.8"
|
||||
android:fontFamily="@font/roboto_regular"
|
||||
android:textColor="?colorOnBackground"
|
||||
tools:text="Отменён" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</layout>
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
@@ -1,59 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.appcompat.widget.LinearLayoutCompat 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"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="@drawable/ic_play_button_circle_background"
|
||||
android:backgroundTint="?colorPrimaryVariant">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_attachment_file"
|
||||
app:tint="@color/a3_700" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
android:layout_marginStart="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="@drawable/ic_play_button_circle_background"
|
||||
android:backgroundTint="@color/a3_200">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_attachment_file"
|
||||
app:tint="@color/a3_700" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/google_sans_regular"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/n1_800"
|
||||
android:textSize="18sp"
|
||||
tools:text="Kids" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.8"
|
||||
android:fontFamily="@font/roboto_regular"
|
||||
android:textColor="@color/n1_800"
|
||||
tools:text="3.28 TB" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/google_sans_regular"
|
||||
android:maxLines="1"
|
||||
android:textColor="?colorOnBackground"
|
||||
android:textSize="18sp"
|
||||
tools:text="Kids" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.8"
|
||||
android:fontFamily="@font/roboto_regular"
|
||||
android:textColor="?colorOnBackground"
|
||||
tools:text="3.28 TB" />
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</layout>
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
@@ -1,16 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</layout>
|
||||
android:layout_height="wrap_content" />
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
@@ -1,16 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</layout>
|
||||
android:layout_height="wrap_content" />
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user