Code saving and stuff (#9)

lil update
This commit is contained in:
2022-03-25 14:20:12 -07:00
committed by GitHub
parent 35cd23318f
commit 8d0cd19322
130 changed files with 4189 additions and 3055 deletions
+34 -36
View File
@@ -1,14 +1,17 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
val login: String = gradleLocalProperties(rootDir).getProperty("vklogin") val login: String = gradleLocalProperties(rootDir).getProperty("vkLogin")
val password: String = gradleLocalProperties(rootDir).getProperty("vkpassword") val password: String = gradleLocalProperties(rootDir).getProperty("vkPassword")
val sdkPackage: String = gradleLocalProperties(rootDir).getProperty("sdkPackage")
val sdkFingerprint: String = gradleLocalProperties(rootDir).getProperty("sdkFingerprint")
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
id("kotlin-kapt") id("kotlin-kapt")
id("kotlin-parcelize") id("kotlin-parcelize")
id("androidx.navigation.safeargs.kotlin")
id("dagger.hilt.android.plugin") id("dagger.hilt.android.plugin")
} }
@@ -34,6 +37,9 @@ android {
getByName("debug") { getByName("debug") {
buildConfigField("String", "vkLogin", login) buildConfigField("String", "vkLogin", login)
buildConfigField("String", "vkPassword", password) buildConfigField("String", "vkPassword", password)
buildConfigField("String", "sdkPackage", sdkPackage)
buildConfigField("String", "sdkFingerprint", sdkFingerprint)
} }
getByName("release") { getByName("release") {
isMinifyEnabled = false isMinifyEnabled = false
@@ -41,6 +47,9 @@ android {
buildConfigField("String", "vkLogin", login) buildConfigField("String", "vkLogin", login)
buildConfigField("String", "vkPassword", password) buildConfigField("String", "vkPassword", password)
buildConfigField("String", "sdkPackage", sdkPackage)
buildConfigField("String", "sdkFingerprint", sdkFingerprint)
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
@@ -49,23 +58,13 @@ android {
} }
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
}
}
buildFeatures { buildFeatures {
dataBinding = true
viewBinding = true viewBinding = true
} }
} }
kapt { kapt {
@@ -78,44 +77,42 @@ kapt {
} }
dependencies { 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("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.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("androidx.appcompat:appcompat:1.4.1")
implementation("com.google.android.material:material:1.5.0-alpha04") implementation("com.google.android.material:material:1.6.0-beta01")
implementation("androidx.core:core-ktx:1.7.0-beta02") implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.preference:preference-ktx:1.1.1") implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation("androidx.recyclerview:recyclerview:1.2.1") implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.cardview:cardview:1.0.0") 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-core:1.5.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android: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-ktx:2.4.2")
implementation("androidx.room:room-runtime:2.3.0") implementation("androidx.room:room-runtime:2.4.2")
kapt("androidx.room:room-compiler:2.3.0") kapt("androidx.room:room-compiler:2.4.2")
implementation("androidx.navigation:navigation-fragment-ktx:2.3.5") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1")
implementation("androidx.navigation:navigation-ui-ktx:2.3.5") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1") implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.4.1")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.3.1") implementation("androidx.lifecycle:lifecycle-common-java8:2.4.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("com.squareup.okhttp3:okhttp:5.0.0-alpha.2") implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.2")
implementation("com.squareup.okhttp3:logging-interceptor: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") implementation("com.google.dagger:hilt-android:2.39.1")
kapt("com.google.dagger:hilt-android-compiler: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") implementation("io.coil-kt:coil:1.4.0")
@@ -134,4 +130,6 @@ dependencies {
implementation("org.jsoup:jsoup:1.14.3") implementation("org.jsoup:jsoup:1.14.3")
implementation("ch.acra:acra:4.11.1") 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")
} }
+5 -1
View File
@@ -17,7 +17,6 @@
android:testOnly="false" android:testOnly="false"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:replace="android:allowBackup"> tools:replace="android:allowBackup">
<activity <activity
android:name=".activity.MainActivity" android:name=".activity.MainActivity"
android:exported="true" android:exported="true"
@@ -29,6 +28,11 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name=".service.MessagesUpdateService"
android:enabled="true"
android:exported="false" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
@@ -1,17 +1,54 @@
package com.meloda.fast.activity package com.meloda.fast.activity
import android.os.Bundle 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.R
import com.meloda.fast.base.BaseActivity import com.meloda.fast.base.BaseActivity
import com.meloda.fast.common.Screens
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : BaseActivity(R.layout.activity_main) { class MainActivity : BaseActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) { private val navigator = object : AppNavigator(this, R.id.root_fragment_container) {
installSplashScreen() override fun setupFragmentTransaction(
super.onCreate(savedInstanceState) 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.* import com.meloda.fast.api.model.attachments.*
@Suppress("RemoveExplicitTypeArguments")
object VKConstants { object VKConstants {
const val GROUP_FIELDS = "description,members_count,counters,status,verified" 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 ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS"
const val API_VERSION = "5.132" const val API_VERSION = "5.132"
const val LP_VERSION = 10
const val VK_APP_ID = "2274003" const val VK_APP_ID = "2274003"
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH" const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
const val FAST_GROUP_ID = -119516304 const val FAST_GROUP_ID = -119516304
const val FAST_APP_ID = "6964679"
object Auth { object Auth {
const val SCOPE = "notify," + const val SCOPE = "notify," +
@@ -38,7 +42,7 @@ object VKConstants {
} }
} }
val restrictedToEditAttachments = listOf( val restrictedToEditAttachments = listOf<Class<out VkAttachment>>(
VkCall::class.java, VkCall::class.java,
VkCurator::class.java, VkCurator::class.java,
VkEvent::class.java, VkEvent::class.java,
@@ -46,6 +50,25 @@ object VKConstants {
VkGraffiti::class.java, VkGraffiti::class.java,
VkGroupCall::class.java, VkGroupCall::class.java,
VkStory::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) { for (baseAttachment in baseAttachments) {
when (baseAttachment.getPreparedType()) { when (baseAttachment.getPreparedType()) {
BaseVkAttachmentItem.AttachmentType.PHOTO -> { BaseVkAttachmentItem.AttachmentType.Photo -> {
val photo = baseAttachment.photo ?: continue val photo = baseAttachment.photo ?: continue
attachments += photo.asVkPhoto() attachments += photo.asVkPhoto()
} }
BaseVkAttachmentItem.AttachmentType.VIDEO -> { BaseVkAttachmentItem.AttachmentType.Video -> {
val video = baseAttachment.video ?: continue val video = baseAttachment.video ?: continue
attachments += video.asVkVideo() attachments += video.asVkVideo()
} }
BaseVkAttachmentItem.AttachmentType.AUDIO -> { BaseVkAttachmentItem.AttachmentType.Audio -> {
val audio = baseAttachment.audio ?: continue val audio = baseAttachment.audio ?: continue
attachments += audio.asVkAudio() attachments += audio.asVkAudio()
} }
BaseVkAttachmentItem.AttachmentType.FILE -> { BaseVkAttachmentItem.AttachmentType.File -> {
val file = baseAttachment.file ?: continue val file = baseAttachment.file ?: continue
attachments += file.asVkFile() attachments += file.asVkFile()
} }
BaseVkAttachmentItem.AttachmentType.LINK -> { BaseVkAttachmentItem.AttachmentType.Link -> {
val link = baseAttachment.link ?: continue val link = baseAttachment.link ?: continue
attachments += link.asVkLink() attachments += link.asVkLink()
} }
BaseVkAttachmentItem.AttachmentType.MINI_APP -> { BaseVkAttachmentItem.AttachmentType.MiniApp -> {
val miniApp = baseAttachment.miniApp ?: continue val miniApp = baseAttachment.miniApp ?: continue
attachments += VkMiniApp( attachments += miniApp.asVkMiniApp()
link = miniApp.app.shareUrl
)
} }
BaseVkAttachmentItem.AttachmentType.VOICE -> { BaseVkAttachmentItem.AttachmentType.Voice -> {
val voiceMessage = baseAttachment.voiceMessage ?: continue val voiceMessage = baseAttachment.voiceMessage ?: continue
attachments += voiceMessage.asVkVoiceMessage() attachments += voiceMessage.asVkVoiceMessage()
} }
BaseVkAttachmentItem.AttachmentType.STICKER -> { BaseVkAttachmentItem.AttachmentType.Sticker -> {
val sticker = baseAttachment.sticker ?: continue val sticker = baseAttachment.sticker ?: continue
attachments += sticker.asVkSticker() attachments += sticker.asVkSticker()
} }
BaseVkAttachmentItem.AttachmentType.GIFT -> { BaseVkAttachmentItem.AttachmentType.Gift -> {
val gift = baseAttachment.gift ?: continue val gift = baseAttachment.gift ?: continue
attachments += gift.asVkGift() attachments += gift.asVkGift()
} }
BaseVkAttachmentItem.AttachmentType.WALL -> { BaseVkAttachmentItem.AttachmentType.Wall -> {
val wall = baseAttachment.wall ?: continue val wall = baseAttachment.wall ?: continue
attachments += wall.asVkWall() attachments += wall.asVkWall()
} }
BaseVkAttachmentItem.AttachmentType.GRAFFITI -> { BaseVkAttachmentItem.AttachmentType.Graffiti -> {
val graffiti = baseAttachment.graffiti ?: continue val graffiti = baseAttachment.graffiti ?: continue
attachments += graffiti.asVkGraffiti() attachments += graffiti.asVkGraffiti()
} }
BaseVkAttachmentItem.AttachmentType.POLL -> { BaseVkAttachmentItem.AttachmentType.Poll -> {
val poll = baseAttachment.poll ?: continue val poll = baseAttachment.poll ?: continue
attachments += VkPoll( attachments += poll.asVkPoll()
id = poll.id
)
} }
BaseVkAttachmentItem.AttachmentType.WALL_REPLY -> { BaseVkAttachmentItem.AttachmentType.WallReply -> {
val wallReply = baseAttachment.wallReply ?: continue val wallReply = baseAttachment.wallReply ?: continue
attachments += VkWallReply( attachments += wallReply.asVkWallReply()
id = wallReply.id
)
} }
BaseVkAttachmentItem.AttachmentType.CALL -> { BaseVkAttachmentItem.AttachmentType.Call -> {
val call = baseAttachment.call ?: continue val call = baseAttachment.call ?: continue
attachments += call.asVkCall() attachments += call.asVkCall()
} }
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> { BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> {
val groupCall = baseAttachment.groupCall ?: continue val groupCall = baseAttachment.groupCall ?: continue
attachments += VkGroupCall( attachments += groupCall.asVkGroupCall()
initiatorId = groupCall.initiator_id
)
} }
BaseVkAttachmentItem.AttachmentType.CURATOR -> { BaseVkAttachmentItem.AttachmentType.Curator -> {
val curator = baseAttachment.curator ?: continue val curator = baseAttachment.curator ?: continue
attachments += curator.asVkCurator() attachments += curator.asVkCurator()
} }
BaseVkAttachmentItem.AttachmentType.EVENT -> { BaseVkAttachmentItem.AttachmentType.Event -> {
val event = baseAttachment.event ?: continue val event = baseAttachment.event ?: continue
attachments += event.asVkEvent() attachments += event.asVkEvent()
} }
BaseVkAttachmentItem.AttachmentType.STORY -> { BaseVkAttachmentItem.AttachmentType.Story -> {
val story = baseAttachment.story ?: continue val story = baseAttachment.story ?: continue
attachments += story.asVkStory() attachments += story.asVkStory()
} }
BaseVkAttachmentItem.AttachmentType.Widget -> {
val widget = baseAttachment.widget ?: continue
attachments += widget.asVkWidget()
}
else -> continue else -> continue
} }
} }
@@ -580,22 +576,22 @@ object VkUtils {
attachmentType: BaseVkAttachmentItem.AttachmentType attachmentType: BaseVkAttachmentItem.AttachmentType
): Drawable? { ): Drawable? {
val resId = when (attachmentType) { val resId = when (attachmentType) {
BaseVkAttachmentItem.AttachmentType.PHOTO -> R.drawable.ic_attachment_photo BaseVkAttachmentItem.AttachmentType.Photo -> R.drawable.ic_attachment_photo
BaseVkAttachmentItem.AttachmentType.VIDEO -> R.drawable.ic_attachment_video BaseVkAttachmentItem.AttachmentType.Video -> R.drawable.ic_attachment_video
BaseVkAttachmentItem.AttachmentType.AUDIO -> R.drawable.ic_attachment_audio BaseVkAttachmentItem.AttachmentType.Audio -> R.drawable.ic_attachment_audio
BaseVkAttachmentItem.AttachmentType.FILE -> R.drawable.ic_attachment_file BaseVkAttachmentItem.AttachmentType.File -> R.drawable.ic_attachment_file
BaseVkAttachmentItem.AttachmentType.LINK -> R.drawable.ic_attachment_link BaseVkAttachmentItem.AttachmentType.Link -> R.drawable.ic_attachment_link
BaseVkAttachmentItem.AttachmentType.VOICE -> R.drawable.ic_attachment_voice BaseVkAttachmentItem.AttachmentType.Voice -> R.drawable.ic_attachment_voice
BaseVkAttachmentItem.AttachmentType.MINI_APP -> R.drawable.ic_attachment_mini_app BaseVkAttachmentItem.AttachmentType.MiniApp -> R.drawable.ic_attachment_mini_app
BaseVkAttachmentItem.AttachmentType.STICKER -> R.drawable.ic_attachment_sticker BaseVkAttachmentItem.AttachmentType.Sticker -> R.drawable.ic_attachment_sticker
BaseVkAttachmentItem.AttachmentType.GIFT -> R.drawable.ic_attachment_gift BaseVkAttachmentItem.AttachmentType.Gift -> R.drawable.ic_attachment_gift
BaseVkAttachmentItem.AttachmentType.WALL -> R.drawable.ic_attachment_wall BaseVkAttachmentItem.AttachmentType.Wall -> R.drawable.ic_attachment_wall
BaseVkAttachmentItem.AttachmentType.GRAFFITI -> R.drawable.ic_attachment_graffiti BaseVkAttachmentItem.AttachmentType.Graffiti -> R.drawable.ic_attachment_graffiti
BaseVkAttachmentItem.AttachmentType.POLL -> R.drawable.ic_attachment_poll BaseVkAttachmentItem.AttachmentType.Poll -> R.drawable.ic_attachment_poll
BaseVkAttachmentItem.AttachmentType.WALL_REPLY -> R.drawable.ic_attachment_wall_reply BaseVkAttachmentItem.AttachmentType.WallReply -> R.drawable.ic_attachment_wall_reply
BaseVkAttachmentItem.AttachmentType.CALL -> R.drawable.ic_attachment_call BaseVkAttachmentItem.AttachmentType.Call -> R.drawable.ic_attachment_call
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> R.drawable.ic_attachment_group_call BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> R.drawable.ic_attachment_group_call
BaseVkAttachmentItem.AttachmentType.STORY -> R.drawable.ic_attachment_story BaseVkAttachmentItem.AttachmentType.Story -> R.drawable.ic_attachment_story
else -> return null else -> return null
} }
@@ -617,24 +613,25 @@ object VkUtils {
fun getAttachmentTypeByClass(attachment: VkAttachment): BaseVkAttachmentItem.AttachmentType? { fun getAttachmentTypeByClass(attachment: VkAttachment): BaseVkAttachmentItem.AttachmentType? {
return when (attachment) { return when (attachment) {
is VkPhoto -> BaseVkAttachmentItem.AttachmentType.PHOTO is VkPhoto -> BaseVkAttachmentItem.AttachmentType.Photo
is VkVideo -> BaseVkAttachmentItem.AttachmentType.VIDEO is VkVideo -> BaseVkAttachmentItem.AttachmentType.Video
is VkAudio -> BaseVkAttachmentItem.AttachmentType.AUDIO is VkAudio -> BaseVkAttachmentItem.AttachmentType.Audio
is VkFile -> BaseVkAttachmentItem.AttachmentType.FILE is VkFile -> BaseVkAttachmentItem.AttachmentType.File
is VkLink -> BaseVkAttachmentItem.AttachmentType.LINK is VkLink -> BaseVkAttachmentItem.AttachmentType.Link
is VkMiniApp -> BaseVkAttachmentItem.AttachmentType.MINI_APP is VkMiniApp -> BaseVkAttachmentItem.AttachmentType.MiniApp
is VkVoiceMessage -> BaseVkAttachmentItem.AttachmentType.VOICE is VkVoiceMessage -> BaseVkAttachmentItem.AttachmentType.Voice
is VkSticker -> BaseVkAttachmentItem.AttachmentType.STICKER is VkSticker -> BaseVkAttachmentItem.AttachmentType.Sticker
is VkGift -> BaseVkAttachmentItem.AttachmentType.GIFT is VkGift -> BaseVkAttachmentItem.AttachmentType.Gift
is VkWall -> BaseVkAttachmentItem.AttachmentType.WALL is VkWall -> BaseVkAttachmentItem.AttachmentType.Wall
is VkGraffiti -> BaseVkAttachmentItem.AttachmentType.GRAFFITI is VkGraffiti -> BaseVkAttachmentItem.AttachmentType.Graffiti
is VkPoll -> BaseVkAttachmentItem.AttachmentType.POLL is VkPoll -> BaseVkAttachmentItem.AttachmentType.Poll
is VkWallReply -> BaseVkAttachmentItem.AttachmentType.WALL_REPLY is VkWallReply -> BaseVkAttachmentItem.AttachmentType.WallReply
is VkCall -> BaseVkAttachmentItem.AttachmentType.CALL is VkCall -> BaseVkAttachmentItem.AttachmentType.Call
is VkGroupCall -> BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS is VkGroupCall -> BaseVkAttachmentItem.AttachmentType.GroupCallInProgress
is VkEvent -> BaseVkAttachmentItem.AttachmentType.EVENT is VkEvent -> BaseVkAttachmentItem.AttachmentType.Event
is VkCurator -> BaseVkAttachmentItem.AttachmentType.CURATOR is VkCurator -> BaseVkAttachmentItem.AttachmentType.Curator
is VkStory -> BaseVkAttachmentItem.AttachmentType.STORY is VkStory -> BaseVkAttachmentItem.AttachmentType.Story
is VkWidget -> BaseVkAttachmentItem.AttachmentType.Widget
else -> null else -> null
} }
} }
@@ -645,42 +642,44 @@ object VkUtils {
size: Int = 1 size: Int = 1
): String { ): String {
return when (attachmentType) { return when (attachmentType) {
BaseVkAttachmentItem.AttachmentType.PHOTO -> BaseVkAttachmentItem.AttachmentType.Photo ->
context.resources.getQuantityString(R.plurals.attachment_photos, size, size) context.resources.getQuantityString(R.plurals.attachment_photos, size, size)
BaseVkAttachmentItem.AttachmentType.VIDEO -> BaseVkAttachmentItem.AttachmentType.Video ->
context.resources.getQuantityString(R.plurals.attachment_videos, size, size) context.resources.getQuantityString(R.plurals.attachment_videos, size, size)
BaseVkAttachmentItem.AttachmentType.AUDIO -> BaseVkAttachmentItem.AttachmentType.Audio ->
context.resources.getQuantityString(R.plurals.attachment_audios, size, size) context.resources.getQuantityString(R.plurals.attachment_audios, size, size)
BaseVkAttachmentItem.AttachmentType.FILE -> BaseVkAttachmentItem.AttachmentType.File ->
context.resources.getQuantityString(R.plurals.attachment_files, size, size) context.resources.getQuantityString(R.plurals.attachment_files, size, size)
BaseVkAttachmentItem.AttachmentType.LINK -> BaseVkAttachmentItem.AttachmentType.Link ->
context.resources.getString(R.string.message_attachments_link) context.resources.getString(R.string.message_attachments_link)
BaseVkAttachmentItem.AttachmentType.VOICE -> BaseVkAttachmentItem.AttachmentType.Voice ->
context.resources.getString(R.string.message_attachments_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) context.resources.getString(R.string.message_attachments_mini_app)
BaseVkAttachmentItem.AttachmentType.STICKER -> BaseVkAttachmentItem.AttachmentType.Sticker ->
context.resources.getString(R.string.message_attachments_sticker) context.resources.getString(R.string.message_attachments_sticker)
BaseVkAttachmentItem.AttachmentType.GIFT -> BaseVkAttachmentItem.AttachmentType.Gift ->
context.resources.getString(R.string.message_attachments_gift) context.resources.getString(R.string.message_attachments_gift)
BaseVkAttachmentItem.AttachmentType.WALL -> BaseVkAttachmentItem.AttachmentType.Wall ->
context.resources.getString(R.string.message_attachments_wall) context.resources.getString(R.string.message_attachments_wall)
BaseVkAttachmentItem.AttachmentType.GRAFFITI -> BaseVkAttachmentItem.AttachmentType.Graffiti ->
context.resources.getString(R.string.message_attachments_graffiti) context.resources.getString(R.string.message_attachments_graffiti)
BaseVkAttachmentItem.AttachmentType.POLL -> BaseVkAttachmentItem.AttachmentType.Poll ->
context.resources.getString(R.string.message_attachments_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) context.resources.getString(R.string.message_attachments_wall_reply)
BaseVkAttachmentItem.AttachmentType.CALL -> BaseVkAttachmentItem.AttachmentType.Call ->
context.resources.getString(R.string.message_attachments_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) context.resources.getString(R.string.message_attachments_call_in_progress)
BaseVkAttachmentItem.AttachmentType.EVENT -> BaseVkAttachmentItem.AttachmentType.Event ->
context.resources.getString(R.string.message_attachments_event) context.resources.getString(R.string.message_attachments_event)
BaseVkAttachmentItem.AttachmentType.CURATOR -> BaseVkAttachmentItem.AttachmentType.Curator ->
context.resources.getString(R.string.message_attachments_curator) context.resources.getString(R.string.message_attachments_curator)
BaseVkAttachmentItem.AttachmentType.STORY -> BaseVkAttachmentItem.AttachmentType.Story ->
context.resources.getString(R.string.message_attachments_story) context.resources.getString(R.string.message_attachments_story)
BaseVkAttachmentItem.AttachmentType.Widget ->
context.resources.getString(R.string.message_attachments_widget)
else -> attachmentType.value else -> attachmentType.value
} }
} }
@@ -1,11 +1,11 @@
package com.meloda.fast.api.model package com.meloda.fast.api.model
import android.os.Parcelable
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.meloda.fast.model.SelectableItem
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -35,7 +35,7 @@ data class VkConversation(
@Embedded(prefix = "lastMessage_") @Embedded(prefix = "lastMessage_")
var lastMessage: VkMessage? = null, var lastMessage: VkMessage? = null,
) : Parcelable { ) : SelectableItem(id) {
@Ignore @Ignore
@IgnoredOnParcel @IgnoredOnParcel
@@ -7,7 +7,7 @@ import androidx.room.PrimaryKey
import com.meloda.fast.api.UserConfig import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.model.attachments.VkAttachment 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 com.meloda.fast.util.TimeUtils
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -33,10 +33,8 @@ data class VkMessage(
var forwards: List<VkMessage>? = null, var forwards: List<VkMessage>? = null,
var attachments: List<VkAttachment>? = null, var attachments: List<VkAttachment>? = null,
// @Embedded(prefix = "replyMessage_")
var replyMessage: VkMessage? = null var replyMessage: VkMessage? = null
) : SelectableItem() { ) : SelectableItem(id) {
@Ignore @Ignore
@IgnoredOnParcel @IgnoredOnParcel
@@ -53,8 +51,8 @@ data class VkMessage(
fun isGroup() = fromId < 0 fun isGroup() = fromId < 0
fun isRead(conversation: VkConversation) = fun isRead(conversation: VkConversation) =
if (isOut) conversation.outRead < id if (isOut) conversation.outRead - id >= 0
else conversation.inRead < id else conversation.inRead - id >= 0
fun getPreparedAction(): Action? { fun getPreparedAction(): Action? {
if (action == null) return null if (action == null) return null
@@ -20,21 +20,30 @@ data class VkPhoto(
val userId: Int? val userId: Int?
) : VkAttachment() { ) : 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 @Ignore
@IgnoredOnParcel @IgnoredOnParcel
private val sizesChars = Stack<Char>() private val sizesChars = Stack<Char>()
init { init {
sizesChars.push('s') sizesChars.push(SIZE_TYPE_75)
sizesChars.push('m') sizesChars.push(SIZE_TYPE_130)
sizesChars.push('x') sizesChars.push(SIZE_TYPE_604)
sizesChars.push('o') sizesChars.push('o')
sizesChars.push('p') sizesChars.push('p')
sizesChars.push('q') sizesChars.push('q')
sizesChars.push('r') sizesChars.push('r')
sizesChars.push('y') sizesChars.push(SIZE_TYPE_807)
sizesChars.push('z') sizesChars.push(SIZE_TYPE_1080_1024)
sizesChars.push('w') sizesChars.push(SIZE_TYPE_2560_2048)
} }
@IgnoredOnParcel @IgnoredOnParcel
@@ -61,7 +70,7 @@ data class VkPhoto(
} }
fun getSizeOrSmaller(type: Char): BaseVkPhoto.Size? { fun getSizeOrSmaller(type: Char): BaseVkPhoto.Size? {
val photoStack = sizesChars.clone() as Stack<Char> val photoStack = sizesChars.clone() as Stack<*>
val sizeIndex = photoStack.search(type) val sizeIndex = photoStack.search(type)
@@ -72,7 +81,7 @@ data class VkPhoto(
} }
for (i in 0 until photoStack.size) { for (i in 0 until photoStack.size) {
val size = getSizeOrNull(photoStack.peek()) val size = getSizeOrNull(photoStack.peek() as Char)
if (size == null) { if (size == null) {
photoStack.pop() photoStack.pop()
@@ -7,5 +7,11 @@ data class VkStory(
val id: Int, val id: Int,
val ownerId: Int, val ownerId: Int,
val date: Int, val date: Int,
val photo: VkPhoto val photo: VkPhoto?
) : VkAttachment() ) : VkAttachment() {
fun isFromUser() = ownerId > 0
fun isFromGroup() = ownerId < 0
}
@@ -1,5 +1,6 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import android.os.Parcelable
import com.meloda.fast.api.VkUtils import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.model.base.attachments.BaseVkVideo import com.meloda.fast.api.model.base.attachments.BaseVkVideo
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
@@ -9,7 +10,7 @@ import kotlinx.parcelize.Parcelize
data class VkVideo( data class VkVideo(
val id: Int, val id: Int,
val ownerId: Int, val ownerId: Int,
val images: List<BaseVkVideo.Image>, val images: List<VideoImage>,
val firstFrames: List<BaseVkVideo.FirstFrame>?, val firstFrames: List<BaseVkVideo.FirstFrame>?,
val accessKey: String? val accessKey: String?
) : VkAttachment() { ) : VkAttachment() {
@@ -17,10 +18,57 @@ data class VkVideo(
@IgnoredOnParcel @IgnoredOnParcel
val className: String = this::class.java.name val className: String = this::class.java.name
fun imageForWidth(width: Int): BaseVkVideo.Image? { fun imageForWidth(width: Int): VideoImage? {
return images.find { it.width == width } 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( override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
attachmentClass = this::class.java, attachmentClass = this::class.java,
id = id, id = id,
@@ -12,8 +12,8 @@ data class VkVoiceMessage(
val linkOgg: String, val linkOgg: String,
val linkMp3: String, val linkMp3: String,
val accessKey: String, val accessKey: String,
val transcriptState: String, val transcriptState: String?,
val transcript: String val transcript: String?
) : VkAttachment() { ) : VkAttachment() {
@IgnoredOnParcel @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 @Parcelize
data class BaseVkMessage( data class BaseVkMessage(
val id: Int,
val peer_id: Int,
val date: Int, val date: Int,
val from_id: Int, val from_id: Int,
val id: Int,
val out: Int, val out: Int,
val peer_id: Int,
val text: String, val text: String,
val conversation_message_id: Int, val conversation_message_id: Int,
val fwd_messages: List<BaseVkMessage>? = listOf(), val fwd_messages: List<BaseVkMessage>? = emptyList(),
val important: Boolean, val important: Boolean,
val random_id: Int, val random_id: Int,
val attachments: List<BaseVkAttachmentItem> = listOf(), val attachments: List<BaseVkAttachmentItem> = emptyList(),
val is_hidden: Boolean, val is_hidden: Boolean,
val payload: String, val payload: String,
val geo: Geo?, val geo: Geo?,
@@ -29,7 +29,7 @@ data class BaseVkMessage(
fun asVkMessage() = VkMessage( fun asVkMessage() = VkMessage(
id = id, id = id,
text = if (text.isBlank()) null else text, text = text.ifBlank { null },
isOut = out == 1, isOut = out == 1,
peerId = peer_id, peerId = peer_id,
fromId = from_id, fromId = from_id,
@@ -30,38 +30,40 @@ data class BaseVkAttachmentItem(
val groupCall: BaseVkGroupCall?, val groupCall: BaseVkGroupCall?,
val curator: BaseVkCurator?, val curator: BaseVkCurator?,
val event: BaseVkEvent?, val event: BaseVkEvent?,
val story: BaseVkStory? val story: BaseVkStory?,
val widget: BaseVkWidget?
) : Parcelable { ) : Parcelable {
fun getPreparedType() = AttachmentType.parse(type) fun getPreparedType() = AttachmentType.parse(type)
enum class AttachmentType(var value: String) { enum class AttachmentType(var value: String) {
UNKNOWN("unknown"), Unknown("unknown"),
PHOTO("photo"), Photo("photo"),
VIDEO("video"), Video("video"),
AUDIO("audio"), Audio("audio"),
FILE("doc"), File("doc"),
LINK("link"), Link("link"),
VOICE("audio_message"), Voice("audio_message"),
MINI_APP("mini_app"), MiniApp("mini_app"),
STICKER("sticker"), Sticker("sticker"),
GIFT("gift"), Gift("gift"),
WALL("wall"), Wall("wall"),
GRAFFITI("graffiti"), Graffiti("graffiti"),
POLL("poll"), Poll("poll"),
WALL_REPLY("wall_reply"), WallReply("wall_reply"),
CALL("call"), Call("call"),
GROUP_CALL_IN_PROGRESS("group_call_in_progress"), GroupCallInProgress("group_call_in_progress"),
CURATOR("curator"), Curator("curator"),
EVENT("event"), Event("event"),
STORY("story") Story("story"),
Widget("widget")
; ;
companion object { companion object {
fun parse(value: String): AttachmentType? { fun parse(value: String): AttachmentType {
val parsedValue = values().firstOrNull { it.value == value } ?: UNKNOWN val parsedValue = values().firstOrNull { it.value == value } ?: Unknown
if (parsedValue == UNKNOWN) { if (parsedValue == Unknown) {
Log.e("AttachmentType", "Unknown attachment type: $value") Log.e("AttachmentType", "Unknown attachment type: $value")
} }
@@ -10,13 +10,11 @@ data class BaseVkEvent(
val is_favorite: Boolean, val is_favorite: Boolean,
val text: String, val text: String,
val address: String, val address: String,
val friends: List<Int> = listOf(), val friends: List<Int> = emptyList(),
val member_status: Int, val member_status: Int,
val time: Int val time: Int
) : BaseVkAttachment() { ) : BaseVkAttachment() {
fun asVkEvent() = VkEvent( fun asVkEvent() = VkEvent(id = id)
id = id
)
} }
@@ -1,6 +1,7 @@
package com.meloda.fast.api.model.base.attachments package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkGroupCall
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -16,4 +17,6 @@ data class BaseVkGroupCall(
val count: Int val count: Int
) : Parcelable ) : Parcelable
fun asVkGroupCall() = VkGroupCall(initiatorId = initiator_id)
} }
@@ -2,6 +2,7 @@ package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.attachments.VkMiniApp
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -63,4 +64,6 @@ data class BaseVkMiniApp(
val url: String val url: String
) : Parcelable ) : Parcelable
fun asVkMiniApp() = VkMiniApp(link = app.shareUrl)
} }
@@ -1,6 +1,7 @@
package com.meloda.fast.api.model.base.attachments package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkPoll
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -55,7 +56,8 @@ data class BaseVkPoll(
val color: String, val color: String,
val position: Double val position: Double
) : Parcelable ) : Parcelable
} }
fun asVkPoll() = VkPoll(id = id)
} }
@@ -17,7 +17,7 @@ data class BaseVkStory(
val date: Int, val date: Int,
val expires_at: Int, val expires_at: Int,
val is_ads: Boolean, val is_ads: Boolean,
val photo: BaseVkPhoto, val photo: BaseVkPhoto?,
val replies: Replies, val replies: Replies,
val is_one_time: Boolean, val is_one_time: Boolean,
val track_code: String, val track_code: String,
@@ -40,7 +40,7 @@ data class BaseVkStory(
id = id, id = id,
ownerId = owner_id, ownerId = owner_id,
date = date, date = date,
photo = photo.asVkPhoto() photo = photo?.asVkPhoto()
) )
@Parcelize @Parcelize
@@ -41,18 +41,26 @@ data class BaseVkVideo(
fun asVkVideo() = VkVideo( fun asVkVideo() = VkVideo(
id = id, id = id,
ownerId = owner_id, ownerId = owner_id,
images = image, images = image.map { it.asVideoImage() },
firstFrames = first_frame, firstFrames = first_frame,
accessKey = access_key accessKey = access_key
) )
@Parcelize @Parcelize
data class Image( data class Image(
val height: Int,
val width: Int, val width: Int,
val height: Int,
val url: String, val url: String,
val with_padding: Int val with_padding: Int?
) : Parcelable ) : Parcelable {
fun asVideoImage() = VkVideo.VideoImage(
width = width,
height = height,
url = url,
withPadding = with_padding == 1
)
}
@Parcelize @Parcelize
data class FirstFrame( data class FirstFrame(
@@ -13,8 +13,8 @@ data class BaseVkVoiceMessage(
val link_ogg: String, val link_ogg: String,
val link_mp3: String, val link_mp3: String,
val access_key: String, val access_key: String,
val transcript_state: String, val transcript_state: String?,
val transcript: String val transcript: String?
) : Parcelable { ) : Parcelable {
fun asVkVoiceMessage() = VkVoiceMessage( fun asVkVoiceMessage() = VkVoiceMessage(
@@ -1,6 +1,7 @@
package com.meloda.fast.api.model.base.attachments package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkWallReply
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -17,7 +18,6 @@ data class BaseVkWallReply(
val reply_to_comment: Int? val reply_to_comment: Int?
) : Parcelable { ) : Parcelable {
@Parcelize @Parcelize
data class Likes( data class Likes(
val count: Int, val count: Int,
@@ -26,4 +26,6 @@ data class BaseVkWallReply(
val can_publish: Int val can_publish: Int
) : Parcelable ) : 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.UserConfig
import com.meloda.fast.api.VKConstants import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.network.account.AccountUrls
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import java.net.URLEncoder import java.net.URLEncoder
@@ -12,6 +13,8 @@ class AuthInterceptor : Interceptor {
val builder = chain.request().url.newBuilder() val builder = chain.request().url.newBuilder()
.addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8")) .addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8"))
if (!builder.build().toUrl().toString().contains(AccountUrls.SetOnline))
UserConfig.accessToken.let { UserConfig.accessToken.let {
if (it.isNotBlank()) if (it.isNotBlank())
builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8")) builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8"))
@@ -5,6 +5,36 @@ object VkUrls {
const val OAUTH = "https://oauth.vk.com" const val OAUTH = "https://oauth.vk.com"
const val API = "https://api.vk.com/method" 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 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) suspend fun sendSms(validationSid: String) = repo.sendSms(validationSid)
@@ -8,9 +8,9 @@ import retrofit2.http.QueryMap
interface AuthRepo { interface AuthRepo {
@GET(AuthUrls.DirectAuth) @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) @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 package com.meloda.fast.api.network.auth
import android.os.Parcelable 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 import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class RequestAuthDirect( data class AuthDirectRequest(
@SerializedName("grant_type") val grantType: String, val grantType: String,
@SerializedName("client_id") val clientId: String, val clientId: String,
@SerializedName("client_secret") val clientSecret: String, val clientSecret: String,
@SerializedName("username") val username: String, val username: String,
@SerializedName("password") val password: String, val password: String,
@SerializedName("scope") val scope: String, val scope: String,
@SerializedName("2fa_supported") val twoFaSupported: Boolean = true, val twoFaSupported: Boolean = true,
@SerializedName("force_sms") val twoFaForceSms: Boolean = false, val twoFaForceSms: Boolean = false,
@SerializedName("code") val twoFaCode: String? = null, val twoFaCode: String? = null,
@SerializedName("captcha_sid") val captchaSid: String? = null, val captchaSid: String? = null,
@SerializedName("captcha_key") val captchaKey: String? = null, val captchaKey: String? = null,
) : Parcelable { ) : Parcelable {
val map val map
get() = mutableMapOf( get() = mutableMapOf(
"grant_type" to grantType, "grant_type" to grantType,
@@ -35,3 +37,31 @@ data class RequestAuthDirect(
captchaKey?.let { this["captcha_key"] = 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 import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class ResponseAuthDirect( data class AuthDirectResponse(
@SerializedName("access_token") val accessToken: String? = null, @SerializedName("access_token") val accessToken: String? = null,
@SerializedName("user_id") val userId: Int? = null, @SerializedName("user_id") val userId: Int? = null,
@SerializedName("trusted_hash") val twoFaHash: String? = null, @SerializedName("trusted_hash") val twoFaHash: String? = null,
@@ -13,7 +13,7 @@ data class ResponseAuthDirect(
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
data class ResponseSendSms( data class SendSmsResponse(
@SerializedName("sid") val validationSid: String?, @SerializedName("sid") val validationSid: String?,
@SerializedName("delay") val delay: Int?, @SerializedName("delay") val delay: Int?,
@SerializedName("validation_type") val validationType: String?, @SerializedName("validation_type") val validationType: String?,
@@ -1,18 +1,17 @@
package com.meloda.fast.api.network.longpoll 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 com.meloda.fast.api.network.Answer
import org.json.JSONObject
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.QueryMap import retrofit2.http.QueryMap
import retrofit2.http.Url
interface LongPollRepo { interface LongPollRepo {
@GET("https://{serverUrl}") @GET
suspend fun getResponse( suspend fun getResponse(
@Path("serverUrl") serverUrl: String, @Url serverUrl: String,
@QueryMap params: Map<String, 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 package com.meloda.fast.api.network.messages
import com.meloda.fast.api.model.VkMessage 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 com.meloda.fast.database.dao.MessagesDao
import javax.inject.Inject import javax.inject.Inject
class MessagesDataSource @Inject constructor( class MessagesDataSource @Inject constructor(
private val repo: MessagesRepo, private val messagesRepo: MessagesRepo,
private val dao: MessagesDao 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) = suspend fun getHistory(params: MessagesGetHistoryRequest) =
repo.getHistory(params.map) messagesRepo.getHistory(params.map)
suspend fun send(params: MessagesSendRequest) = suspend fun send(params: MessagesSendRequest) =
repo.send(params.map) messagesRepo.send(params.map)
suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) = suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) =
repo.markAsImportant(params.map) messagesRepo.markAsImportant(params.map)
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) = suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
repo.getLongPollServer(params.map) messagesRepo.getLongPollServer(params.map)
suspend fun pin(params: MessagesPinMessageRequest) = suspend fun pin(params: MessagesPinMessageRequest) =
repo.pin(params.map) messagesRepo.pin(params.map)
suspend fun unpin(params: MessagesUnPinMessageRequest) = suspend fun unpin(params: MessagesUnPinMessageRequest) =
repo.unpin(params.map) messagesRepo.unpin(params.map)
suspend fun delete(params: MessagesDeleteRequest) = suspend fun delete(params: MessagesDeleteRequest) =
repo.delete(params.map) messagesRepo.delete(params.map)
suspend fun edit(params: MessagesEditRequest) = suspend fun edit(params: MessagesEditRequest) =
repo.edit(params.map) messagesRepo.edit(params.map)
suspend fun store(messages: List<VkMessage>) = dao.insert(messages) suspend fun getLongPollUpdates(
serverUrl: String,
suspend fun getCached(peerId: Int) = dao.getByPeerId(peerId) 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) @POST(MessagesUrls.Edit)
suspend fun edit(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>> 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 replyTo: Int? = null,
val stickerId: Int? = null, val stickerId: Int? = null,
val disableMentions: Boolean? = null, val disableMentions: Boolean? = null,
val dontParseLinks: Boolean? = null val dontParseLinks: Boolean? = null,
val silent: Boolean? = null
) : Parcelable { ) : Parcelable {
val map val map
@@ -55,6 +56,7 @@ data class MessagesSendRequest(
stickerId?.let { this["sticker_id"] = it.toString() } stickerId?.let { this["sticker_id"] = it.toString() }
disableMentions?.let { this["disable_mentions"] = it.intString } disableMentions?.let { this["disable_mentions"] = it.intString }
dontParseLinks?.let { this["dont_parse_links"] = it.intString } dontParseLinks?.let { this["dont_parse_links"] = it.intString }
silent?.let { this["silent"] = it.toString() }
} }
} }
@@ -166,3 +168,20 @@ 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 @Parcelize
data class MessagesGetHistoryResponse( data class MessagesGetHistoryResponse(
val count: Int, val count: Int,
val items: List<BaseVkMessage> = listOf(), val items: List<BaseVkMessage> = emptyList(),
val conversations: List<BaseVkConversation>?, val conversations: List<BaseVkConversation>?,
val profiles: List<BaseVkUser>?, val profiles: List<BaseVkUser>?,
val groups: List<BaseVkGroup>? val groups: List<BaseVkGroup>?
) : Parcelable ) : 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 Unpin = "${VkUrls.API}/messages.unpin"
const val Delete = "${VkUrls.API}/messages.delete" const val Delete = "${VkUrls.API}/messages.delete"
const val Edit = "${VkUrls.API}/messages.edit" const val Edit = "${VkUrls.API}/messages.edit"
const val GetById = "${VkUrls.API}/messages.getById"
} }
@@ -1,40 +1,12 @@
package com.meloda.fast.base package com.meloda.fast.base
import android.os.Bundle
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity 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() : super()
constructor(@LayoutRes resId: Int) : super(resId) 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 package com.meloda.fast.base.adapter
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter 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") @Suppress("MemberVisibilityCanBePrivate", "unused", "UNCHECKED_CAST")
abstract class BaseAdapter<Item, VH : BaseHolder>( @SuppressLint("NotifyDataSetChanged")
abstract class BaseAdapter<T : DataItem<*>, VH : BaseHolder> constructor(
var context: Context, var context: Context,
values: MutableList<Item>, diffUtil: DiffUtil.ItemCallback<T>,
diffUtil: DiffUtil.ItemCallback<Item> preAddedValues: List<T> = emptyList(),
) : ListAdapter<Item, VH>(diffUtil) { ) : ListAdapter<T, VH>(diffUtil) {
val cleanValues = mutableListOf<Item>() protected val adapterScope = CoroutineScope(Dispatchers.Default)
val values = mutableListOf<Item>() private val cleanList = mutableListOf<T>()
init {
addAll(values)
}
protected var inflater: LayoutInflater = LayoutInflater.from(context) protected var inflater: LayoutInflater = LayoutInflater.from(context)
var itemClickListener: ((position: Int) -> Unit) = {} var itemClickListener: ((position: Int) -> Unit)? = null
var itemLongClickListener: ((position: Int) -> Boolean) = { false } var itemLongClickListener: ((position: Int) -> Boolean)? = null
init {
cleanList.addAll(preAddedValues)
addAll(preAddedValues)
}
fun cloneCurrentList(): MutableList<T> {
return ArrayList(currentList)
}
open fun destroy() {} open fun destroy() {}
override fun getItem(position: Int): Item { fun getOrNull(position: Int): T? {
return values[position] return if (position >= 0 && position <= currentList.lastIndex) get(position) else null
} }
fun getOrNull(position: Int): Item? { fun getOrElse(position: Int, defaultValue: (Int) -> T): T {
return if (position >= 0 && position <= values.lastIndex) get(position) else null return if (position >= 0 && position <= currentList.lastIndex) get(position)
}
fun getOrElse(position: Int, defaultValue: (Int) -> Item): Item {
return if (position >= 0 && position <= values.lastIndex) get(position)
else defaultValue(position) else defaultValue(position)
} }
fun add(position: Int, item: Item) { fun add(
values.add(position, item) item: T,
cleanValues.add(position, item) 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()
} }
fun add(item: Item) { if (beforeFooter) {
values += item mutableItems += DataItem.Footer as T
cleanValues.add(item)
} }
fun addAll(items: List<Item>) { newList.addAll(mutableItems)
values += items cleanList.addAll(mutableItems)
cleanValues.addAll(items) } else {
newList.addAll(position, items)
cleanList.addAll(position, items)
} }
fun addAll(position: Int, items: List<Item>) { withContext(Dispatchers.Main) {
values.addAll(position, items) submitList(newList, commitCallback)
cleanValues.addAll(position, items) }
}
} }
fun removeAll(items: List<Item>) { fun remove(item: T, commitCallback: (() -> Unit)? = null) =
values.removeAll(items) removeAll(listOf(item), commitCallback)
cleanValues.removeAll(items)
fun removeAll(items: List<T>, commitCallback: (() -> Unit)? = null) {
val newList = cloneCurrentList()
newList.removeAll(items)
submitList(newList, commitCallback)
cleanList.removeAll(items)
} }
fun removeAt(index: Int) { fun removeAt(index: Int, commitCallback: (() -> Unit)? = null) {
values.removeAt(index) val newList = cloneCurrentList()
cleanValues.removeAt(index) newList.removeAt(index)
submitList(newList, commitCallback)
cleanList.removeAt(index)
} }
fun remove(item: Item) { fun clear(commitCallback: (() -> Unit)? = null) = removeAll(currentList, commitCallback)
values.remove(item)
cleanValues.remove(item) 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 clear() { fun indexOf(item: T): Int {
values.clear() return currentList.indexOf(item)
cleanValues.clear()
} }
operator fun get(position: Int): Item { val indices get() = currentList.indices
return values[position]
operator fun get(position: Int): T {
return currentList[position]
} }
operator fun set(position: Int, item: Item) { operator fun set(position: Int, item: T) = setItem(position, item)
values[position] = item
cleanValues[position] = item fun setItem(position: Int, item: T, commitCallback: (() -> Unit)? = null) {
val newList = cloneCurrentList()
newList[position] = item
submitList(newList, commitCallback)
cleanList[position] = item
} }
open fun notifyChanges(oldList: List<Item>, newList: List<Item>) {} fun isEmpty() = currentList.isEmpty()
fun isNotEmpty() = currentList.isNotEmpty()
fun isEmpty() = values.isEmpty() @SuppressLint("NotifyDataSetChanged")
fun isNotEmpty() = values.isNotEmpty() fun refreshList() {
notifyDataSetChanged()
fun view(resId: Int, viewGroup: ViewGroup, attachToRoot: Boolean = false): View {
return inflater.inflate(resId, viewGroup, attachToRoot)
} }
fun updateValues(list: MutableList<Item>) { fun updateCleanList(list: List<T>?) {
values.clear() cleanList.clear()
values += list list?.run { cleanList.addAll(this) }
}
override fun submitList(list: List<T>?) {
super.submitList(list)
updateCleanList(list)
}
override fun submitList(list: List<T>?, commitCallback: Runnable?) {
super.submitList(list, commitCallback)
updateCleanList(list)
} }
override fun onBindViewHolder(holder: VH, position: Int) { override fun onBindViewHolder(holder: VH, position: Int) {
onBindItemViewHolder(holder, position)
}
private fun onBindItemViewHolder(holder: VH, position: Int) {
initListeners(holder.itemView, position) initListeners(holder.itemView, position)
holder.bind(position) holder.bind(position)
} }
@@ -117,15 +189,16 @@ abstract class BaseAdapter<Item, VH : BaseHolder>(
protected open fun initListeners(itemView: View, position: Int) { protected open fun initListeners(itemView: View, position: Int) {
if (itemView is AdapterView<*>) return if (itemView is AdapterView<*>) return
itemView.setOnClickListener { itemClickListener.invoke(position) } itemView.setOnClickListener { itemClickListener?.invoke(position) }
itemView.setOnLongClickListener { itemLongClickListener.invoke(position) } itemView.setOnLongClickListener {
itemLongClickListener?.invoke(position)
return@setOnLongClickListener itemClickListener != null
}
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return values.size return currentList.size
} }
val lastPosition val lastPosition get() = currentList.lastIndex
get() = itemCount - 1
} }
@@ -5,6 +5,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.meloda.fast.extensions.dpToPx
import com.meloda.fast.util.AndroidUtils import com.meloda.fast.util.AndroidUtils
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -24,7 +25,7 @@ class EmptyHeaderAdapter(
private fun generateHeaderView() = View(context).apply { private fun generateHeaderView() = View(context).apply {
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
AndroidUtils.px(56).roundToInt() 56.dpToPx()
) )
isClickable = false isClickable = false
isEnabled = 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
}
@@ -17,3 +17,7 @@ object StartProgressEvent : VkEvent()
object StopProgressEvent : 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 preferences: SharedPreferences
lateinit var resources: Resources lateinit var resources: Resources
lateinit var packageName: String lateinit var packageName: String
lateinit var instance: AppGlobal private lateinit var instance: AppGlobal
lateinit var appDatabase: AppDatabase lateinit var appDatabase: AppDatabase
@@ -41,6 +41,8 @@ class AppGlobal : Application() {
var screenWidth = 0 var screenWidth = 0
var screenHeight = 0 var screenHeight = 0
val Instance get() = instance
} }
override fun onCreate() { override fun onCreate() {
@@ -51,9 +53,7 @@ class AppGlobal : Application() {
ACRA.init(this) ACRA.init(this)
} }
appDatabase = Room.databaseBuilder( appDatabase = Room.databaseBuilder(this, AppDatabase::class.java, "cache")
this, AppDatabase::class.java, "cache"
)
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .build()
@@ -85,10 +85,8 @@ class AppGlobal : Application() {
"width: $screenWidth; height: $screenHeight; density: $density; diagonal: $diagonal; dpiDensity: $densityDpi; scaledDensity: $densityScaled; xDpi: $xDpi; yDpi: $yDpi" "width: $screenWidth; height: $screenHeight; density: $density; diagonal: $diagonal; dpiDensity: $densityDpi; scaledDensity: $densityScaled; xDpi: $xDpi; yDpi: $yDpi"
) )
inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 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, VkUser::class,
VkGroup::class VkGroup::class
], ],
version = 26, version = 28,
exportSchema = false, exportSchema = false,
) )
@TypeConverters(Converters::class) @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.VkMessage
import com.meloda.fast.api.model.attachments.VkAttachment import com.meloda.fast.api.model.attachments.VkAttachment
import org.json.JSONObject import org.json.JSONObject
import java.util.stream.Collectors
@Suppress("UnnecessaryVariable")
class Converters { class Converters {
private companion object {
private const val CACHE_SEPARATOR = "fastkruta228355"
}
@TypeConverter @TypeConverter
fun fromListVkMessageToString(messages: List<VkMessage>?): String? { fun fromListVkMessageToString(messages: List<VkMessage>?): String? {
if (messages == null) return null if (messages == null) return null
val string = val string = messages.map { fromVkMessageToString(it)!! }.joinToString { CACHE_SEPARATOR }
messages.map { fromVkMessageToString(it)!! }.stream()
.collect(Collectors.joining("fastkruta228355"))
return string return string
} }
@@ -24,9 +26,9 @@ class Converters {
fun fromStringToListVkMessage(string: String?): List<VkMessage>? { fun fromStringToListVkMessage(string: String?): List<VkMessage>? {
if (string == null) return null if (string == null) return null
if (string.contains("fastkruta228355")) { if (string.contains(CACHE_SEPARATOR)) {
val messages = val messages =
string.split("fastkruta228355").map { fromStringToVkMessage(it)!! } string.split(CACHE_SEPARATOR).map { fromStringToVkMessage(it)!! }
return messages return messages
} }
@@ -55,8 +57,7 @@ class Converters {
if (attachments == null) return null if (attachments == null) return null
val string = val string =
attachments.map { fromVkAttachmentToString(it)!! }.stream() attachments.map { fromVkAttachmentToString(it)!! }.joinToString { CACHE_SEPARATOR }
.collect(Collectors.joining("fastkruta228355"))
return string return string
} }
@@ -65,9 +66,9 @@ class Converters {
fun fromStringToListVkAttachment(string: String?): List<VkAttachment>? { fun fromStringToListVkAttachment(string: String?): List<VkAttachment>? {
if (string == null) return null if (string == null) return null
if (string.contains("fastkruta228355")) { if (string.contains(CACHE_SEPARATOR)) {
val attachments = val attachments =
string.split("fastkruta228355").map { fromStringToVkAttachment(it)!! } string.split(CACHE_SEPARATOR).map { fromStringToVkAttachment(it)!! }
return attachments 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.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.meloda.fast.api.LongPollUpdatesParser
import com.meloda.fast.api.network.AuthInterceptor import com.meloda.fast.api.network.AuthInterceptor
import com.meloda.fast.api.network.ResultCallFactory 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.AuthDataSource
import com.meloda.fast.api.network.auth.AuthRepo
import com.meloda.fast.api.network.conversations.ConversationsDataSource import com.meloda.fast.api.network.conversations.ConversationsDataSource
import com.meloda.fast.api.network.conversations.ConversationsRepo import com.meloda.fast.api.network.conversations.ConversationsRepo
import com.meloda.fast.api.network.longpoll.LongPollRepo import com.meloda.fast.api.network.longpoll.LongPollRepo
import com.meloda.fast.api.network.messages.MessagesDataSource 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.messages.MessagesRepo
import com.meloda.fast.api.network.users.UsersDataSource
import com.meloda.fast.api.network.users.UsersRepo import com.meloda.fast.api.network.users.UsersRepo
import com.meloda.fast.database.dao.ConversationsDao import com.meloda.fast.database.dao.ConversationsDao
import com.meloda.fast.database.dao.MessagesDao import com.meloda.fast.database.dao.MessagesDao
@@ -67,22 +70,27 @@ object NetworkModule {
fun provideAuthInterceptor(): AuthInterceptor = AuthInterceptor() fun provideAuthInterceptor(): AuthInterceptor = AuthInterceptor()
@Provides @Provides
@Singleton
fun provideAuthRepo(retrofit: Retrofit): AuthRepo = fun provideAuthRepo(retrofit: Retrofit): AuthRepo =
retrofit.create(AuthRepo::class.java) retrofit.create(AuthRepo::class.java)
@Provides @Provides
@Singleton
fun provideConversationsRepo(retrofit: Retrofit): ConversationsRepo = fun provideConversationsRepo(retrofit: Retrofit): ConversationsRepo =
retrofit.create(ConversationsRepo::class.java) retrofit.create(ConversationsRepo::class.java)
@Provides @Provides
@Singleton
fun provideUsersRepo(retrofit: Retrofit): UsersRepo = fun provideUsersRepo(retrofit: Retrofit): UsersRepo =
retrofit.create(UsersRepo::class.java) retrofit.create(UsersRepo::class.java)
@Provides @Provides
@Singleton
fun provideMessagesRepo(retrofit: Retrofit): MessagesRepo = fun provideMessagesRepo(retrofit: Retrofit): MessagesRepo =
retrofit.create(MessagesRepo::class.java) retrofit.create(MessagesRepo::class.java)
@Provides @Provides
@Singleton
fun provideLongPollRepo(retrofit: Retrofit): LongPollRepo = fun provideLongPollRepo(retrofit: Retrofit): LongPollRepo =
retrofit.create(LongPollRepo::class.java) retrofit.create(LongPollRepo::class.java)
@@ -109,7 +117,27 @@ object NetworkModule {
@Provides @Provides
@Singleton @Singleton
fun provideMessagesDataSource( fun provideMessagesDataSource(
repo: MessagesRepo, messagesRepo: MessagesRepo,
dao: MessagesDao messagesDao: MessagesDao,
): MessagesDataSource = MessagesDataSource(repo, dao) 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.text.style.ForegroundColorSpan
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.util.ObjectsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.setPadding import androidx.core.view.setPadding
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import coil.load
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.api.UserConfig import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants import com.meloda.fast.api.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.BaseAdapter
import com.meloda.fast.base.adapter.BindingHolder import com.meloda.fast.base.adapter.BindingHolder
import com.meloda.fast.databinding.ItemConversationBinding 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 import com.meloda.fast.util.TimeUtils
class ConversationsAdapter constructor( class ConversationsAdapter constructor(
context: Context, context: Context,
values: MutableList<VkConversation>, private val resourceManager: ConversationsResourceManager,
var isMultilineEnabled: Boolean = true,
val profiles: HashMap<Int, VkUser> = hashMapOf(), val profiles: HashMap<Int, VkUser> = hashMapOf(),
val groups: HashMap<Int, VkGroup> = hashMapOf(), val groups: HashMap<Int, VkGroup> = hashMapOf(),
var isMultilineEnabled: Boolean = true ) : BaseAdapter<VkConversation, ConversationsAdapter.ItemHolder>(context, Comparator) {
) : BaseAdapter<VkConversation, ConversationsAdapter.ItemHolder>(
context, values, COMPARATOR
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = var pinnedCount = 0
ItemHolder(ItemConversationBinding.inflate(inflater, parent, false))
inner class ItemHolder(binding: ItemConversationBinding) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder {
BindingHolder<ItemConversationBinding>(binding) { return ItemHolder(
ItemConversationBinding.inflate(inflater, parent, false),
resourceManager
)
}
private val dateColor = ContextCompat.getColor(context, R.color.n2_500) inner class ItemHolder(
private val youPrefix = context.getString(R.string.you_message_prefix) binding: ItemConversationBinding,
private val resourceManager: ConversationsResourceManager
) : BindingHolder<ItemConversationBinding>(binding) {
init { init {
binding.title.ellipsize = TextUtils.TruncateAt.END binding.title.ellipsize = TextUtils.TruncateAt.END
@@ -69,7 +78,7 @@ class ConversationsAdapter constructor(
) )
val span = SpannableString(text) 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 binding.message.text = span
return return
@@ -87,51 +96,46 @@ class ConversationsAdapter constructor(
conversationGroup = conversationGroup conversationGroup = conversationGroup
) )
binding.avatar.isVisible = avatar != null binding.avatar.toggleVisibility(avatar != null)
if (avatar == null) { if (avatar == null) {
binding.avatarPlaceholder.isVisible = true binding.avatarPlaceholder.visible()
if (conversation.ownerId == VKConstants.FAST_GROUP_ID) { if (conversation.ownerId == VKConstants.FAST_GROUP_ID) {
binding.placeholderBack.setImageDrawable( binding.placeholderBack.loadWithGlide(
ColorDrawable( drawable = ColorDrawable(resourceManager.icLauncherColor),
ContextCompat.getColor(context, R.color.a1_400) transformations = ImageLoader.userAvatarTransformations
)
) )
binding.placeholder.imageTintList = 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.setImageResource(R.drawable.ic_fast_logo)
binding.placeholder.setPadding(18) binding.placeholder.setPadding(18)
} else { } else {
binding.placeholderBack.setImageDrawable( binding.placeholderBack.loadWithGlide(
ColorDrawable( drawable = ColorDrawable(resourceManager.colorOnUserAvatarAction),
ContextCompat.getColor(context, R.color.n1_50) transformations = ImageLoader.userAvatarTransformations
)
) )
binding.placeholder.imageTintList = 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.setImageResource(R.drawable.ic_account_circle_cut)
binding.placeholder.setPadding(0) binding.placeholder.setPadding(0)
binding.avatar.setImageDrawable(null) binding.avatar.clear()
} }
} else { } else {
binding.avatar.load(avatar) { binding.avatar.loadWithGlide(
crossfade(200) url = avatar,
target { crossFade = true,
binding.avatarPlaceholder.isVisible = false onLoadedAction = { binding.avatarPlaceholder.gone() }
binding.avatar.setImageDrawable(it) )
}
}
} }
binding.online.isVisible = conversationUser?.online == true binding.online.toggleVisibility(conversationUser?.online == true)
binding.pin.toggleVisibility(conversation.isPinned)
binding.pin.isVisible = conversation.isPinned
val actionMessage = VkUtils.getActionConversationText( val actionMessage = VkUtils.getActionConversationText(
context = context, context = context,
message = message, message = message,
youPrefix = youPrefix, youPrefix = resourceManager.youPrefix,
profiles = profiles, profiles = profiles,
groups = groups, groups = groups,
messageUser = messageUser, messageUser = messageUser,
@@ -150,7 +154,7 @@ class ConversationsAdapter constructor(
message = message message = message
) )
binding.textAttachment.isVisible = attachmentIcon != null binding.textAttachment.toggleVisibility(attachmentIcon != null)
binding.textAttachment.setImageDrawable(attachmentIcon) binding.textAttachment.setImageDrawable(attachmentIcon)
val attachmentText = if (attachmentIcon == null) VkUtils.getAttachmentText( val attachmentText = if (attachmentIcon == null) VkUtils.getAttachmentText(
@@ -174,7 +178,7 @@ class ConversationsAdapter constructor(
var prefix = when { var prefix = when {
actionMessage != null -> "" actionMessage != null -> ""
message.isOut -> "$youPrefix: " message.isOut -> "${resourceManager.youPrefix}: "
else -> { else -> {
if (message.isUser() && messageUser != null && messageUser.firstName.isNotBlank()) "${messageUser.firstName}: " if (message.isUser() && messageUser != null && messageUser.firstName.isNotBlank()) "${messageUser.firstName}: "
else if (message.isGroup() && messageGroup != null && messageGroup.name.isNotBlank()) "${messageGroup.name}: " else if (message.isGroup() && messageGroup != null && messageGroup.name.isNotBlank()) "${messageGroup.name}: "
@@ -190,7 +194,7 @@ class ConversationsAdapter constructor(
val spanMessage = SpannableString(spanText) val spanMessage = SpannableString(spanText)
spanMessage.setSpan( spanMessage.setSpan(
ForegroundColorSpan(dateColor), 0, ForegroundColorSpan(resourceManager.colorOutline), 0,
prefix.length + coloredMessage.length, prefix.length + coloredMessage.length,
0 0
) )
@@ -208,6 +212,15 @@ class ConversationsAdapter constructor(
R.drawable.ic_message_unread R.drawable.ic_message_unread
) else null ) else null
binding.onlineBorder.setImageDrawable(
ColorDrawable(
ContextCompat.getColor(
context,
if (conversation.isUnread()) R.color.colorBackgroundVariant
else R.color.colorBackground
)
)
)
binding.counter.isVisible = conversation.isInUnread() binding.counter.isVisible = conversation.isInUnread()
if (conversation.isInUnread()) { if (conversation.isInUnread()) {
@@ -222,10 +235,10 @@ class ConversationsAdapter constructor(
} }
fun removeConversation(conversationId: Int): Int? { fun removeConversation(conversationId: Int): Int? {
for (i in values.indices) { for (i in indices) {
val conversation = values[i] val conversation = getItem(i)
if (conversation.id == conversationId) { if (conversation.id == conversationId) {
values.removeAt(i) removeAt(i)
return i return i
} }
} }
@@ -233,17 +246,29 @@ class ConversationsAdapter constructor(
return null 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 { companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<VkConversation>() { private val Comparator = object : DiffUtil.ItemCallback<VkConversation>() {
override fun areItemsTheSame( override fun areItemsTheSame(
oldItem: VkConversation, oldItem: VkConversation,
newItem: VkConversation newItem: VkConversation
) = false ): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame( override fun areContentsTheSame(
oldItem: VkConversation, oldItem: VkConversation,
newItem: VkConversation newItem: VkConversation
) = false ) = ObjectsCompat.equals(oldItem, newItem)
} }
} }
@@ -1,23 +1,16 @@
package com.meloda.fast.screens.conversations package com.meloda.fast.screens.conversations
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import android.viewbinding.library.fragment.viewBinding import android.viewbinding.library.fragment.viewBinding
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import coil.load
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.activity.MainActivity
import com.meloda.fast.api.UserConfig import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.base.BaseViewModelFragment import com.meloda.fast.base.BaseViewModelFragment
@@ -28,14 +21,16 @@ import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.AppSettings import com.meloda.fast.common.AppSettings
import com.meloda.fast.common.dataStore import com.meloda.fast.common.dataStore
import com.meloda.fast.databinding.FragmentConversationsBinding import com.meloda.fast.databinding.FragmentConversationsBinding
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 com.meloda.fast.util.AndroidUtils
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.roundToInt
@AndroidEntryPoint @AndroidEntryPoint
class ConversationsFragment : class ConversationsFragment :
@@ -47,9 +42,7 @@ class ConversationsFragment :
private val adapter: ConversationsAdapter by lazy { private val adapter: ConversationsAdapter by lazy {
ConversationsAdapter( ConversationsAdapter(
requireContext(), requireContext(),
mutableListOf(), ConversationsResourceManager(requireContext())
hashMapOf(),
hashMapOf()
).also { ).also {
it.itemClickListener = this::onItemClick it.itemClickListener = this::onItemClick
it.itemLongClickListener = this::onItemLongClick 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
prepareViews() prepareViews()
@@ -90,41 +76,16 @@ class ConversationsFragment :
lifecycleScope.launch { lifecycleScope.launch {
requireContext().dataStore.data.map { requireContext().dataStore.data.map {
adapter.isMultilineEnabled = it[AppSettings.keyIsMultilineEnabled] ?: true adapter.isMultilineEnabled = it[AppSettings.keyIsMultilineEnabled] ?: true
adapter.notifyItemRangeChanged(0, adapter.itemCount) adapter.refreshList()
}.collect() }.collect()
} }
binding.createChat.setOnClickListener {} binding.createChat.setOnClickListener {}
UserConfig.vkUser.observe(viewLifecycleOwner) { UserConfig.vkUser.observe(viewLifecycleOwner) { user ->
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } } 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.setOnClickListener { avatarPopupMenu.show() }
binding.avatar.setOnLongClickListener { binding.avatar.setOnLongClickListener {
@@ -134,23 +95,18 @@ class ConversationsFragment :
settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled
adapter.isMultilineEnabled = !isMultilineEnabled adapter.isMultilineEnabled = !isMultilineEnabled
adapter.notifyItemRangeChanged(0, adapter.itemCount) adapter.refreshList()
} }
} }
true true
} }
if (isPaused) {
isPaused = false
return
}
viewModel.loadProfileUser() viewModel.loadProfileUser()
viewModel.loadConversations() viewModel.loadConversations()
} }
private fun showLogOutDialog() { private fun showLogOutDialog() {
val isEasterEgg = UserConfig.userId == UserConfig.userId val isEasterEgg = UserConfig.userId == 37610580
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle( .setTitle(
@@ -166,13 +122,7 @@ class ConversationsFragment :
UserConfig.clear() UserConfig.clear()
AppGlobal.appDatabase.clearAllTables() AppGlobal.appDatabase.clearAllTables()
requireActivity().finishAffinity() viewModel.openRootScreen()
requireActivity().startActivity(
Intent(
requireContext(),
MainActivity::class.java
)
)
} }
} }
.setNegativeButton(R.string.no, null) .setNegativeButton(R.string.no, null)
@@ -185,21 +135,31 @@ class ConversationsFragment :
is StartProgressEvent -> onProgressStarted() is StartProgressEvent -> onProgressStarted()
is StopProgressEvent -> onProgressStopped() is StopProgressEvent -> onProgressStopped()
is ConversationsLoaded -> refreshConversations(event) is ConversationsLoadedEvent -> refreshConversations(event)
is ConversationsDelete -> deleteConversation(event.peerId) is ConversationsDeleteEvent -> deleteConversation(event.peerId)
// TODO: 10-Oct-21 remove this and sort conversations list // 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() { private fun onProgressStarted() {
binding.progressBar.isVisible = adapter.isEmpty() binding.progressBar.toggleVisibility(adapter.isEmpty())
binding.refreshLayout.isRefreshing = adapter.isNotEmpty() binding.refreshLayout.isRefreshing = adapter.isNotEmpty()
} }
private fun onProgressStopped() { private fun onProgressStopped() {
binding.progressBar.isVisible = false binding.progressBar.gone()
binding.refreshLayout.isRefreshing = false 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.profiles += event.profiles
adapter.groups += event.groups adapter.groups += event.groups
val pinnedConversations = event.conversations.filter { it.isPinned }
adapter.pinnedCount = pinnedConversations.count()
fillRecyclerView(event.conversations) fillRecyclerView(event.conversations)
} }
private fun fillRecyclerView(values: List<VkConversation>) { private fun fillRecyclerView(values: List<VkConversation>) {
adapter.values.clear()
adapter.values += values
adapter.submitList(values) adapter.submitList(values)
} }
@@ -257,12 +218,11 @@ class ConversationsFragment :
if (conversation.isGroup()) adapter.groups[conversation.id] if (conversation.isGroup()) adapter.groups[conversation.id]
else null else null
findNavController().navigate( viewModel.openMessagesHistoryScreen(
R.id.toMessagesHistory,
bundleOf( bundleOf(
"conversation" to adapter[position], MessagesHistoryFragment.ARG_USER to user,
"user" to user, MessagesHistoryFragment.ARG_GROUP to group,
"group" to group MessagesHistoryFragment.ARG_CONVERSATION to conversation
) )
) )
} }
@@ -277,7 +237,7 @@ class ConversationsFragment :
var canPinOneMoreDialog = true var canPinOneMoreDialog = true
if (adapter.itemCount > 4) { if (adapter.itemCount > 4) {
val firstFiveDialogs = adapter.values.subList(0, 5) val firstFiveDialogs = adapter.currentList.subList(0, 5)
var pinnedCount = 0 var pinnedCount = 0
firstFiveDialogs.forEach { if (it.isPinned) pinnedCount++ } firstFiveDialogs.forEach { if (it.isPinned) pinnedCount++ }
@@ -321,8 +281,7 @@ class ConversationsFragment :
} }
private fun deleteConversation(conversationId: Int) { private fun deleteConversation(conversationId: Int) {
val index = adapter.removeConversation(conversationId) ?: return adapter.removeConversation(conversationId)
adapter.notifyItemRemoved(index)
} }
private fun showPinConversationDialog(conversation: VkConversation) { private fun showPinConversationDialog(conversation: VkConversation) {
@@ -345,4 +304,45 @@ class ConversationsFragment :
.show() .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
}
}
} }
@@ -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)
}
@@ -1,28 +1,49 @@
package com.meloda.fast.screens.conversations package com.meloda.fast.screens.conversations
import android.os.Bundle
import androidx.lifecycle.viewModelScope 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.UserConfig
import com.meloda.fast.api.VKConstants import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.network.conversations.* import com.meloda.fast.api.network.conversations.*
import com.meloda.fast.api.network.users.UsersDataSource import com.meloda.fast.api.network.users.UsersDataSource
import com.meloda.fast.api.network.users.UsersGetRequest import com.meloda.fast.api.network.users.UsersGetRequest
import com.meloda.fast.base.viewmodel.BaseViewModel import com.meloda.fast.base.viewmodel.BaseViewModel
import com.meloda.fast.base.viewmodel.VkEvent import com.meloda.fast.base.viewmodel.VkEvent
import com.meloda.fast.common.Screens
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ConversationsViewModel @Inject constructor( class ConversationsViewModel @Inject constructor(
private val conversations: ConversationsDataSource, private val conversations: ConversationsDataSource,
private val users: UsersDataSource private val users: UsersDataSource,
updatesParser: LongPollUpdatesParser,
private val router: Router
) : BaseViewModel() { ) : BaseViewModel() {
companion object {
private const val TAG = "ConversationsViewModel"
}
init {
updatesParser.onNewMessage {
viewModelScope.launch { handleNewMessage(it) }
}
updatesParser.onMessageEdited {
viewModelScope.launch { handleEditedMessage(it) }
}
}
fun loadConversations( fun loadConversations(
offset: Int? = null offset: Int? = null
) = viewModelScope.launch(Dispatchers.Default) { ) = viewModelScope.launch(Dispatchers.Default) {
@@ -49,7 +70,7 @@ class ConversationsViewModel @Inject constructor(
} }
sendEvent( sendEvent(
ConversationsLoaded( ConversationsLoadedEvent(
count = response.count, count = response.count,
offset = offset, offset = offset,
unreadCount = response.unreadCount ?: 0, unreadCount = response.unreadCount ?: 0,
@@ -84,7 +105,7 @@ class ConversationsViewModel @Inject constructor(
conversations.delete( conversations.delete(
ConversationsDeleteRequest(peerId) ConversationsDeleteRequest(peerId)
) )
}, onAnswer = { sendEvent(ConversationsDelete(peerId)) }) }, onAnswer = { sendEvent(ConversationsDeleteEvent(peerId)) })
} }
fun pinConversation( fun pinConversation(
@@ -94,18 +115,41 @@ class ConversationsViewModel @Inject constructor(
if (pin) { if (pin) {
makeJob( makeJob(
{ conversations.pin(ConversationsPinRequest(peerId)) }, { conversations.pin(ConversationsPinRequest(peerId)) },
onAnswer = { sendEvent(ConversationsPin(peerId)) } onAnswer = { sendEvent(ConversationsPinEvent(peerId)) }
) )
} else { } else {
makeJob( makeJob(
{ conversations.unpin(ConversationsUnpinRequest(peerId)) }, { 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
)
)
} }
data class ConversationsLoaded( 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 ConversationsLoadedEvent(
val count: Int, val count: Int,
val offset: Int?, val offset: Int?,
val unreadCount: Int?, val unreadCount: Int?,
@@ -114,8 +158,16 @@ data class ConversationsLoaded(
val groups: HashMap<Int, VkGroup> val groups: HashMap<Int, VkGroup>
) : VkEvent() ) : 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.core.widget.addTextChangedListener
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import coil.load import coil.load
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@@ -35,9 +34,7 @@ import com.meloda.fast.databinding.FragmentLoginBinding
import com.meloda.fast.util.KeyboardUtils import com.meloda.fast.util.KeyboardUtils
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jsoup.Jsoup
import java.net.URLEncoder import java.net.URLEncoder
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
@@ -77,7 +74,7 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
is ErrorEvent -> showErrorSnackbar(event.errorText) is ErrorEvent -> showErrorSnackbar(event.errorText)
is CaptchaEvent -> showCaptchaDialog(event.sid, event.image) is CaptchaEvent -> showCaptchaDialog(event.sid, event.image)
is ValidationEvent -> showValidationRequired(event.sid) is ValidationEvent -> showValidationRequired(event.sid)
is SuccessAuth -> goToMain(event) is SuccessAuth -> launchWebView()
is CodeSent -> showValidationDialog() is CodeSent -> showValidationDialog()
is StartProgressEvent -> onProgressStarted() is StartProgressEvent -> onProgressStarted()
@@ -119,12 +116,8 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
parseAuthUrl(url) parseAuthUrl(url)
} }
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String?) {
super.onPageFinished(view, url) 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() { private fun launchWebView() {
binding.webView.isVisible = true
binding.webView.loadUrl( binding.webView.loadUrl(
"https://oauth.vk.com/authorize?client_id=${UserConfig.FAST_APP_ID}&" + "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=${ "redirect_uri=${
URLEncoder.encode( URLEncoder.encode(
"https://oauth.vk.com/blank.html", "https://oauth.vk.com/blank.html",
Charsets.UTF_8.toString() 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 val token = authData.first
UserConfig.fastToken = token UserConfig.fastToken = token
viewModel.openPrimaryScreen()
} }
} }
@@ -205,9 +208,9 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
else TextInputLayout.END_ICON_NONE else TextInputLayout.END_ICON_NONE
} }
binding.passwordInput.setOnEditorActionListener { _, _, event -> binding.passwordInput.setOnEditorActionListener edit@{ _, _, event ->
if (event == null) return@setOnEditorActionListener false if (event == null) return@edit false
return@setOnEditorActionListener if (event.action == EditorInfo.IME_ACTION_GO || 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)) (event.action == KeyEvent.ACTION_DOWN && (event.keyCode == KeyEvent.KEYCODE_ENTER || event.keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER))
) { ) {
KeyboardUtils.hideKeyboardFrom(binding.passwordInput) KeyboardUtils.hideKeyboardFrom(binding.passwordInput)
@@ -237,7 +240,6 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
KeyboardUtils.hideKeyboardFrom(requireView().findFocus()) KeyboardUtils.hideKeyboardFrom(requireView().findFocus())
viewModel.login( viewModel.login(
login = loginString, login = loginString,
password = passwordString password = passwordString
@@ -383,16 +385,4 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
snackbar.animationMode = Snackbar.ANIMATION_MODE_FADE snackbar.animationMode = Snackbar.ANIMATION_MODE_FADE
snackbar.show() 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 package com.meloda.fast.screens.login
import androidx.lifecycle.viewModelScope 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.VKConstants
import com.meloda.fast.api.VKException import com.meloda.fast.api.VKException
import com.meloda.fast.api.network.auth.AuthDataSource import com.meloda.fast.api.network.auth.AuthDataSource
import com.meloda.fast.api.network.auth.RequestAuthDirect import com.meloda.fast.api.network.auth.AuthDirectRequest
import com.meloda.fast.base.viewmodel.* 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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class LoginViewModel @Inject constructor( class LoginViewModel @Inject constructor(
private val dataSource: AuthDataSource private val dataSource: AuthDataSource,
private val router: Router
) : BaseViewModel() { ) : BaseViewModel() {
companion object {
private const val TAG = "LoginViewModel"
}
fun login( fun login(
login: String, login: String,
password: String, password: String,
@@ -24,7 +34,7 @@ class LoginViewModel @Inject constructor(
makeJob( makeJob(
{ {
dataSource.auth( dataSource.auth(
RequestAuthDirect( AuthDirectRequest(
grantType = VKConstants.Auth.GrantType.PASSWORD, grantType = VKConstants.Auth.GrantType.PASSWORD,
clientId = VKConstants.VK_APP_ID, clientId = VKConstants.VK_APP_ID,
clientSecret = VKConstants.VK_SECRET, clientSecret = VKConstants.VK_SECRET,
@@ -44,12 +54,24 @@ class LoginViewModel @Inject constructor(
return@makeJob return@makeJob
} }
sendEvent( UserConfig.userId = it.userId
SuccessAuth( UserConfig.accessToken = it.accessToken
userId = it.userId,
vkToken = it.accessToken sendEvent(SuccessAuth())
)
) // TODO: 19-Oct-21 do somewhen
// makeJob({
// dataSource.authWithApp(
// AuthWithAppRequest(
// accessToken = it.accessToken
// )
// )
// }, onAnswer = { kindaAnswer ->
// println("$TAG: AppAuthResponse: $kindaAnswer")
// }
// )
}, },
onError = { onError = {
if (it !is VKException) { if (it !is VKException) {
@@ -69,12 +91,14 @@ class LoginViewModel @Inject constructor(
) )
} }
fun openPrimaryScreen() {
router.navigateTo(Screens.Conversations())
}
} }
object CodeSent : VkEvent() object CodeSent : VkEvent()
data class SuccessAuth( data class SuccessAuth(
val haveAuthorized: Boolean = true, val haveAuthorized: Boolean = true
val userId: Int,
val vkToken: String
) : VkEvent() ) : VkEvent()
@@ -1,45 +1,29 @@
package com.meloda.fast.screens.main package com.meloda.fast.screens.main
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.viewbinding.library.fragment.viewBinding import android.view.ViewGroup
import androidx.fragment.app.viewModels 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.base.BaseViewModelFragment
import com.meloda.fast.databinding.FragmentMainBinding
import com.meloda.fast.extensions.NavigationExtensions.setupWithNavController
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class MainFragment : BaseViewModelFragment<MainViewModel>(R.layout.fragment_main) { class MainFragment : BaseViewModelFragment<MainViewModel>() {
override val viewModel: MainViewModel by viewModels() 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
if (!UserConfig.isLoggedIn()) findNavController().navigate(R.id.toLogin) viewModel.checkSession(requireContext())
else if (savedInstanceState == null) setupBottomBar()
}
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 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.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 package com.meloda.fast.screens.messages
import android.content.Context 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.graphics.drawable.ColorDrawable
import android.util.Log import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.Space import android.widget.Space
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.LinearLayoutCompat import androidx.appcompat.widget.LinearLayoutCompat
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isNotEmpty import androidx.core.view.*
import androidx.core.view.isVisible import com.bumptech.glide.Priority
import androidx.core.view.setPadding import com.bumptech.glide.load.engine.DiskCacheStrategy
import androidx.core.view.updatePadding
import coil.load
import com.google.android.material.imageview.ShapeableImageView
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.api.UserConfig import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VkUtils 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.VkUser
import com.meloda.fast.api.model.attachments.* import com.meloda.fast.api.model.attachments.*
import com.meloda.fast.databinding.* 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.util.AndroidUtils
import com.meloda.fast.widget.RoundedFrameLayout
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -46,10 +43,18 @@ class AttachmentInflater constructor(
private val inflater = LayoutInflater.from(context) private val inflater = LayoutInflater.from(context)
private val playColor = ContextCompat.getColor(context, R.color.a3_700) private val colorBackground = ContextCompat.getColor(
private val playBackgroundColor = ContextCompat.getColor(context, R.color.a3_200) 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 { fun setPhotoClickListener(unit: ((url: String) -> Unit)?): AttachmentInflater {
this.photoClickListener = unit this.photoClickListener = unit
@@ -57,15 +62,15 @@ class AttachmentInflater constructor(
} }
fun inflate() { fun inflate() {
if (message.attachments.isNullOrEmpty()) return
attachments = message.attachments!!
container.removeAllViews() container.removeAllViews()
if (textContainer.childCount > 1) { if (textContainer.childCount > 1) {
textContainer.removeViews(1, textContainer.childCount - 1) textContainer.removeViews(1, textContainer.childCount - 1)
} }
if (message.attachments.isNullOrEmpty()) return
attachments = message.attachments!!
if (attachments.size == 1) { if (attachments.size == 1) {
when (val attachment = attachments[0]) { when (val attachment = attachments[0]) {
is VkSticker -> return sticker(attachment) is VkSticker -> return sticker(attachment)
@@ -74,6 +79,7 @@ class AttachmentInflater constructor(
is VkCall -> return call(attachment) is VkCall -> return call(attachment)
is VkGraffiti -> return graffiti(attachment) is VkGraffiti -> return graffiti(attachment)
is VkGift -> return gift(attachment) is VkGift -> return gift(attachment)
is VkStory -> return story(attachment)
} }
} }
@@ -113,112 +119,107 @@ class AttachmentInflater constructor(
} }
private fun photo(photo: VkPhoto) { 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 { val specRatio = size.width.toFloat() / size.height.toFloat()
layoutParams = LinearLayoutCompat.LayoutParams( val widthMultiplier: Float = when {
// ViewGroup.LayoutParams.MATCH_PARENT, specRatio > 1 -> 0.7F
size.width, specRatio < 1 -> 0.45F
size.height else -> 0.35F
// AndroidUtils.px(size.width).roundToInt(), }
// AndroidUtils.px(size.height).roundToInt() val ratio = "${size.width}:${size.height}"
val spacer = Space(context).apply {
layoutParams =
LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5.dpToPx())
}
if (container.isNotEmpty()) {
container.addView(spacer)
}
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
) )
shapeAppearanceModel =
shapeAppearanceModel.withCornerSize {
AndroidUtils.px(5)
} }
scaleType = ImageView.ScaleType.CENTER_CROP binding.image.run {
} shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius * 0.8F)
if (photoClickListener != null) { if (photoClickListener != null) {
newPhoto.setOnClickListener { photoClickListener?.invoke(size.url) } setOnClickListener { photoClickListener?.invoke(size.url) }
} else { } else {
newPhoto.setOnClickListener(null) setOnClickListener(null)
} }
val spacer = Space(context).also { loadWithGlide(
it.layoutParams = LinearLayoutCompat.LayoutParams( url = size.url,
ViewGroup.LayoutParams.MATCH_PARENT, crossFade = true,
AndroidUtils.px(5).roundToInt() placeholderDrawable = ColorDrawable(colorBackground),
priority = Priority.LOW
) )
} }
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())
}
roundedLayout.addView(newPhoto)
container.addView(roundedLayout)
} else {
container.addView(newPhoto)
}
newPhoto.load(size.url) { crossfade(100) }
} }
private fun video(video: VkVideo) { 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 { val spacer = Space(context).apply {
layoutParams = LinearLayoutCompat.LayoutParams( layoutParams =
ViewGroup.LayoutParams.MATCH_PARENT, LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5.dpToPx())
AndroidUtils.px(5).roundToInt() }
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()) binding.image.run {
container.addView(spacer) shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius * 0.8F)
container.addView(layout) loadWithGlide(
url = size.url,
newPhoto.load(size.url) { crossfade(100) } crossFade = true,
placeholderDrawable = ColorDrawable(colorBackground),
priority = Priority.LOW
)
}
} }
private fun audio(audio: VkAudio) { private fun audio(audio: VkAudio) {
@@ -245,14 +246,14 @@ class AttachmentInflater constructor(
val binding = ItemMessageAttachmentLinkBinding.inflate(inflater, textContainer, true) val binding = ItemMessageAttachmentLinkBinding.inflate(inflater, textContainer, true)
binding.title.text = link.title binding.title.text = link.title
binding.title.isVisible = !link.title.isNullOrBlank() binding.title.toggleVisibility(!link.title.isNullOrBlank())
binding.caption.text = link.caption binding.caption.text = link.caption
binding.caption.isVisible = !link.caption.isNullOrBlank() binding.caption.toggleVisibility(!link.caption.isNullOrBlank())
link.photo?.getSizeOrSmaller('y')?.let { link.photo?.getSizeOrSmaller('y')?.let { size ->
binding.preview.load(it.url) { crossfade(150) } binding.preview.loadWithGlide(url = size.url, crossFade = true)
binding.linkIcon.isVisible = false binding.linkIcon.gone()
return return
} }
@@ -264,7 +265,7 @@ class AttachmentInflater constructor(
) )
) )
) )
binding.linkIcon.isVisible = true binding.linkIcon.visible()
} }
private fun sticker(sticker: VkSticker) { private fun sticker(sticker: VkSticker) {
@@ -272,13 +273,12 @@ class AttachmentInflater constructor(
val url = sticker.urlForSize(352) val url = sticker.urlForSize(352)
with(binding.image) { binding.image.run {
layoutParams = LinearLayoutCompat.LayoutParams( val size = 140.dpToPx()
AndroidUtils.px(140).roundToInt(),
AndroidUtils.px(140).roundToInt()
)
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.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) { if (binding.avatar.isVisible) {
binding.avatar.load(avatar) { crossfade(150) } binding.avatar.loadWithGlide(url = avatar, crossFade = true)
} else { } else {
binding.avatar.setImageDrawable(null) binding.avatar.clear()
} }
binding.title.text = title binding.title.text = title
@@ -328,12 +328,13 @@ class AttachmentInflater constructor(
private fun voice(voiceMessage: VkVoiceMessage) { private fun voice(voiceMessage: VkVoiceMessage) {
val binding = ItemMessageAttachmentVoiceBinding.inflate(inflater, textContainer, true) val binding = ItemMessageAttachmentVoiceBinding.inflate(inflater, textContainer, true)
if (message.isOut) if (message.isOut) {
val padding = 6.dpToPx()
binding.root.updatePadding( binding.root.updatePadding(
bottom = AndroidUtils.px(6).roundToInt(), bottom = padding,
left = AndroidUtils.px(6).roundToInt() left = padding
) )
}
val waveform = IntArray(voiceMessage.waveform.size) val waveform = IntArray(voiceMessage.waveform.size)
voiceMessage.waveform.forEachIndexed { index, i -> waveform[index] = i } voiceMessage.waveform.forEachIndexed { index, i -> waveform[index] = i }
@@ -352,8 +353,8 @@ class AttachmentInflater constructor(
if (message.isOut) if (message.isOut)
binding.root.updatePadding( binding.root.updatePadding(
bottom = AndroidUtils.px(5).roundToInt(), bottom = 5.dpToPx(),
left = AndroidUtils.px(6).roundToInt() left = 6.dpToPx()
) )
val callType = val callType =
@@ -383,15 +384,17 @@ class AttachmentInflater constructor(
val url = graffiti.url 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( layoutParams = LinearLayoutCompat.LayoutParams(
AndroidUtils.px(140).roundToInt(), size,
(graffiti.height / heightCoefficient).roundToInt() (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 val url = gift.thumb256 ?: gift.thumb96 ?: gift.thumb48
with(binding.image) { binding.image.run {
shapeAppearanceModel = shapeAppearanceModel.withCornerSize { AndroidUtils.px(12) } val size = 140.dpToPx()
layoutParams = LinearLayoutCompat.LayoutParams( shapeAppearanceModel = shapeAppearanceModel.withCornerSize(12.dpToPx().toFloat())
AndroidUtils.px(140).roundToInt(),
AndroidUtils.px(140).roundToInt() 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
) )
load(url) { crossfade(150) } 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 package com.meloda.fast.screens.messages
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.util.Log
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast 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.api.model.attachments.VkPhoto
import com.meloda.fast.base.adapter.BaseAdapter import com.meloda.fast.base.adapter.BaseAdapter
import com.meloda.fast.base.adapter.BaseHolder import com.meloda.fast.base.adapter.BaseHolder
import com.meloda.fast.databinding.* import com.meloda.fast.databinding.ItemMessageInBinding
import com.meloda.fast.util.AndroidUtils import com.meloda.fast.databinding.ItemMessageOutBinding
import java.util.* import com.meloda.fast.databinding.ItemMessageServiceBinding
import kotlin.math.roundToInt import com.meloda.fast.extensions.dpToPx
import com.meloda.fast.model.DataItem
class MessagesHistoryAdapter constructor( class MessagesHistoryAdapter constructor(
context: Context, context: Context,
values: MutableList<VkMessage>,
val conversation: VkConversation, val conversation: VkConversation,
val profiles: HashMap<Int, VkUser> = hashMapOf(), val profiles: HashMap<Int, VkUser> = hashMapOf(),
val groups: HashMap<Int, VkGroup> = hashMapOf() val groups: HashMap<Int, VkGroup> = hashMapOf()
) : BaseAdapter<VkMessage, MessagesHistoryAdapter.BasicHolder>(context, values, COMPARATOR) { ) : BaseAdapter<DataItem<Int>, MessagesHistoryAdapter.BasicHolder>(context, Comparator) {
var avatarLongClickListener: ((position: Int) -> Unit)? = null var avatarLongClickListener: ((position: Int) -> Unit)? = null
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
when { return when (val item = getItem(position)) {
isPositionHeader(position) -> return HEADER is VkMessage -> {
isPositionFooter(position) -> return FOOTER return when {
item.action != null -> TypeService
item.isOut -> TypeOutgoing
!item.isOut -> TypeIncoming
else -> -1
} }
getItem(position).let { message ->
if (message.action != null) return SERVICE
if (message.isOut) return OUTGOING
if (!message.isOut) return INCOMING
} }
is DataItem.Header -> {
return -1 return TypeHeader
}
is DataItem.Footer -> {
return TypeFooter
}
else -> -1
}
} }
private fun isPositionHeader(position: Int) = position == 0
private fun isPositionFooter(position: Int) = position >= actualSize
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicHolder {
return when (viewType) { return when (viewType) {
// magick numbers is great! // magick numbers is great!
HEADER -> Header(createEmptyView(60)) TypeHeader -> {
FOOTER -> Footer(createEmptyView(36)) Header(createEmptyView(60))
SERVICE -> ServiceMessage( }
TypeFooter -> {
Footer(
createEmptyView(
context.resources.getDimensionPixelSize(R.dimen.messages_history_input_panel_height_with_margins)
)
)
}
TypeService -> ServiceMessage(
ItemMessageServiceBinding.inflate(inflater, parent, false) ItemMessageServiceBinding.inflate(inflater, parent, false)
) )
OUTGOING -> OutgoingMessage( TypeOutgoing -> OutgoingMessage(
ItemMessageOutBinding.inflate(inflater, parent, false) ItemMessageOutBinding.inflate(inflater, parent, false)
) )
INCOMING -> IncomingMessage( TypeIncoming -> IncomingMessage(
ItemMessageInBinding.inflate(inflater, parent, false) ItemMessageInBinding.inflate(inflater, parent, false)
) )
else -> throw IllegalStateException("Wrong viewType: $viewType") else -> throw IllegalStateException("Wrong viewType: $viewType")
} }
} }
// override fun initListeners(itemView: View, position: Int) { override fun onBindViewHolder(holder: BasicHolder, position: Int) {
// if (itemView is AdapterView<*>) return if (holder is Header || holder is Footer) {
// Log.d(
// itemView.setOnClickListener { onItemClickListener?.invoke(position, itemView) } "MessagesHistoryAdapter",
// itemView.setOnLongClickListener { itemLongClickListener.invoke(position) } "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 initListeners(holder.itemView, position)
holder.bind(position)
override fun getItemCount(): Int {
if (actualSize == 0) return 2
return super.getItemCount() + 2
} }
private fun createEmptyView(size: Int) = View(context).apply { private fun createEmptyView(size: Int) = View(context).apply {
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
AndroidUtils.px(size).roundToInt() size
) )
isEnabled = false isEnabled = false
@@ -96,13 +111,6 @@ class MessagesHistoryAdapter constructor(
isFocusable = false 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) open inner class BasicHolder(v: View = View(context)) : BaseHolder(v)
inner class Header(v: View) : BasicHolder(v) inner class Header(v: View) : BasicHolder(v)
@@ -114,10 +122,10 @@ class MessagesHistoryAdapter constructor(
) : BasicHolder(binding.root) { ) : BasicHolder(binding.root) {
override fun bind(position: Int) { override fun bind(position: Int) {
val message = getItem(position) val message = getItem(position) as VkMessage
val prevMessage = getOrNull(position - 1) val prevMessage = getVkMessage(getOrNull(position - 1))
val nextMessage = getOrNull(position + 1) val nextMessage = getVkMessage(getOrNull(position + 1))
MessagesPreparator( MessagesPreparator(
context = context, context = context,
@@ -159,9 +167,8 @@ class MessagesHistoryAdapter constructor(
) : BasicHolder(binding.root) { ) : BasicHolder(binding.root) {
override fun bind(position: Int) { override fun bind(position: Int) {
val message = getItem(position) val message = getItem(position) as VkMessage
val prevMessage = getVkMessage(getOrNull(position - 1))
val prevMessage = getOrNull(position - 1)
MessagesPreparator( MessagesPreparator(
context = context, context = context,
@@ -192,13 +199,12 @@ class MessagesHistoryAdapter constructor(
private val youPrefix = context.getString(R.string.you_message_prefix) private val youPrefix = context.getString(R.string.you_message_prefix)
init { init {
binding.photo.shapeAppearanceModel.run { binding.photo.shapeAppearanceModel =
withCornerSize { AndroidUtils.px(4) } binding.photo.shapeAppearanceModel.withCornerSize(4.dpToPx().toFloat())
}
} }
override fun bind(position: Int) { override fun bind(position: Int) {
val message = getItem(position) val message = getItem(position) as VkMessage
val messageUser = val messageUser =
if (message.isUser()) profiles[message.fromId] if (message.isUser()) profiles[message.fromId]
@@ -241,59 +247,56 @@ class MessagesHistoryAdapter constructor(
} }
} }
fun removeMessageById(id: Int): Int? { fun getVkMessage(item: DataItem<*>?): VkMessage? {
for (i in values.indices) { if (item == null) return null
val message = values[i] if (item is VkMessage) return item
if (message.id == id) {
values.removeAt(i) return null
return i
} }
fun searchMessageIndex(messageId: Int): Int? {
for (i in indices) {
val message = getItem(i)
if (message is VkMessage && message.id == messageId) return i
} }
return null return null
} }
fun removeMessagesByIds(ids: List<Int>): List<Int> { fun searchMessageById(messageId: Int): VkMessage? {
val positions = mutableListOf<Int>() for (i in indices) {
val message = getItem(i)
for (i in values.indices) { if (message is VkMessage && message.id == messageId) return message
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
} }
return null return null
} }
companion object { companion object {
private const val SERVICE = 1 private const val TypeService = 1
private const val HEADER = 0 private const val TypeHeader = 0
private const val FOOTER = 2 private const val TypeFooter = 2
private const val INCOMING = 3 private const val TypeIncoming = 3
private const val OUTGOING = 4 private const val TypeOutgoing = 4
private val Comparator = object : DiffUtil.ItemCallback<DataItem<Int>>() {
private val COMPARATOR = object : DiffUtil.ItemCallback<VkMessage>() {
override fun areItemsTheSame( override fun areItemsTheSame(
oldItem: VkMessage, oldItem: DataItem<Int>,
newItem: VkMessage newItem: DataItem<Int>
) = false ): 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( override fun areContentsTheSame(
oldItem: VkMessage, oldItem: DataItem<Int>,
newItem: VkMessage newItem: DataItem<Int>
) = false ): Boolean = oldItem == newItem
} }
} }
} }
@@ -1,16 +1,21 @@
package com.meloda.fast.screens.messages package com.meloda.fast.screens.messages
import android.animation.ValueAnimator
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.View import android.view.View
import android.view.animation.LinearInterpolator
import android.viewbinding.library.fragment.viewBinding import android.viewbinding.library.fragment.viewBinding
import android.widget.Toast import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.setPadding import androidx.core.view.setPadding
import androidx.core.view.updateLayoutParams
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -32,7 +37,9 @@ import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VkEvent import com.meloda.fast.base.viewmodel.VkEvent
import com.meloda.fast.databinding.DialogMessageDeleteBinding import com.meloda.fast.databinding.DialogMessageDeleteBinding
import com.meloda.fast.databinding.FragmentMessagesHistoryBinding 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.AndroidUtils
import com.meloda.fast.util.TimeUtils import com.meloda.fast.util.TimeUtils
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -41,10 +48,26 @@ import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.math.roundToInt import kotlin.math.roundToInt
@AndroidEntryPoint @AndroidEntryPoint
class MessagesHistoryFragment : class MessagesHistoryFragment :
BaseViewModelFragment<MessagesHistoryViewModel>(R.layout.fragment_messages_history) { 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() override val viewModel: MessagesHistoryViewModel by viewModels()
private val binding: FragmentMessagesHistoryBinding by viewBinding() private val binding: FragmentMessagesHistoryBinding by viewBinding()
@@ -55,21 +78,20 @@ class MessagesHistoryFragment :
} }
private val user: VkUser? by lazy { private val user: VkUser? by lazy {
requireArguments().getParcelable("user") requireArguments().getParcelable(ARG_USER)
} }
private val group: VkGroup? by lazy { private val group: VkGroup? by lazy {
requireArguments().getParcelable("group") requireArguments().getParcelable(ARG_GROUP)
} }
private val conversation: VkConversation by lazy { private val conversation: VkConversation by lazy {
requireNotNull(requireArguments().getParcelable("conversation")) requireNotNull(requireArguments().getParcelable(ARG_CONVERSATION))
} }
private val adapter: MessagesHistoryAdapter by lazy { private val adapter: MessagesHistoryAdapter by lazy {
MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also { MessagesHistoryAdapter(requireContext(), conversation).also {
it.itemClickListener = this::onItemClick it.itemClickListener = this::onItemClick
it.itemLongClickListener = this::onItemLongClick
it.avatarLongClickListener = this::onAvatarLongClickListener it.avatarLongClickListener = this::onAvatarLongClickListener
} }
} }
@@ -90,6 +112,8 @@ class MessagesHistoryFragment :
else -> null else -> null
} }
binding.back.setOnClickListener { requireActivity().onBackPressed() }
binding.title.ellipsize = TextUtils.TruncateAt.END binding.title.ellipsize = TextUtils.TruncateAt.END
binding.status.ellipsize = TextUtils.TruncateAt.END binding.status.ellipsize = TextUtils.TruncateAt.END
@@ -121,7 +145,7 @@ class MessagesHistoryFragment :
binding.action.setOnClickListener { performAction() } 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 if (bottom >= oldBottom) return@addOnLayoutChangeListener
val lastVisiblePosition = val lastVisiblePosition =
(binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() (binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
@@ -138,8 +162,8 @@ class MessagesHistoryFragment :
val firstPosition = val firstPosition =
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() (recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
val message = adapter.getOrNull(firstPosition) adapter.getOrNull(firstPosition)?.let {
message?.let { if (it !is VkMessage) return
binding.timestamp.isVisible = true binding.timestamp.isVisible = true
val time = "${ val time = "${
@@ -158,7 +182,7 @@ class MessagesHistoryFragment :
timestampTimer = Timer() timestampTimer = Timer()
timestampTimer?.schedule(2500) { timestampTimer?.schedule(2500) {
recyclerView.post { binding.timestamp.isVisible = false } recyclerView.post { binding.timestamp.gone() }
} }
} }
@@ -185,6 +209,8 @@ class MessagesHistoryFragment :
.scaleY(1.25f) .scaleY(1.25f)
.setDuration(100) .setDuration(100)
.withEndAction { .withEndAction {
if (getView() == null) return@withEndAction
binding.action.animate() binding.action.animate()
.scaleX(1f) .scaleX(1f)
.scaleY(1f) .scaleY(1f)
@@ -209,21 +235,37 @@ class MessagesHistoryFragment :
} }
} }
attachmentController.isPanelVisible.observe(viewLifecycleOwner) { attachmentController.isPanelVisible.observe(viewLifecycleOwner) { isVisible ->
if (it) binding.message.setSelection(binding.message.text.toString().length) if (isVisible) binding.message.setSelection(binding.message.text.toString().length)
val layoutParams = binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams val currentMargin =
layoutParams.bottomMargin = (binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin
if (it) (binding.attachmentPanel.height / 1.5).roundToInt() else 0
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@{ binding.attachmentPanel.setOnClickListener c@{
val message = attachmentController.message.value ?: return@c val message = attachmentController.message.value ?: return@c
val index = adapter.values.indexOf(message) val index = adapter.indexOf(message)
if (index == -1) return@c if (index == -1) return@c
binding.recyclerView.smoothScrollToPosition(index) binding.recyclerView.scrollToPosition(index)
} }
binding.dismissReply.setOnClickListener { 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() { private fun prepareAvatar() {
val avatar = when { val avatar = when {
conversation.ownerId == VKConstants.FAST_GROUP_ID -> null conversation.ownerId == VKConstants.FAST_GROUP_ID -> null
@@ -241,46 +288,49 @@ class MessagesHistoryFragment :
else -> null 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) { if (avatar == null) {
binding.avatarPlaceholder.isVisible = true binding.avatarPlaceholder.visible()
if (conversation.ownerId == VKConstants.FAST_GROUP_ID) { if (conversation.ownerId == VKConstants.FAST_GROUP_ID) {
binding.placeholderBack.setImageDrawable( binding.placeholderBack.loadWithGlide(
ColorDrawable( drawable = ColorDrawable(icLauncherColor),
ContextCompat.getColor(requireContext(), R.color.a1_400) transformations = ImageLoader.userAvatarTransformations
)
) )
binding.placeholder.imageTintList = 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.setImageResource(R.drawable.ic_fast_logo)
binding.placeholder.setPadding(18) binding.placeholder.setPadding(18)
} else { } else {
binding.placeholderBack.setImageDrawable( binding.placeholderBack.loadWithGlide(
ColorDrawable( drawable = ColorDrawable(colorOnUserAvatarAction),
ContextCompat.getColor(requireContext(), R.color.n1_50) transformations = ImageLoader.userAvatarTransformations
)
) )
binding.placeholder.imageTintList = 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.setImageResource(R.drawable.ic_account_circle_cut)
binding.placeholder.setPadding(0) binding.placeholder.setPadding(0)
binding.avatar.setImageDrawable(null) binding.avatar.clear()
} }
} else { } else {
binding.avatar.load(avatar) { binding.avatar.load(avatar) {
crossfade(200) crossfade(200)
target { target {
binding.avatarPlaceholder.isVisible = false binding.avatarPlaceholder.gone()
binding.avatar.setImageDrawable(it) binding.avatar.setImageDrawable(it)
} }
} }
} }
binding.phantomIcon.isVisible = conversation.isPhantom binding.phantomIcon.toggleVisibility(conversation.isPhantom)
binding.online.isVisible = user?.online == true binding.online.toggleVisibility(user?.online)
binding.pin.isVisible = conversation.isPinned
} }
private fun performAction() { private fun performAction() {
@@ -293,8 +343,10 @@ class MessagesHistoryFragment :
val date = System.currentTimeMillis() val date = System.currentTimeMillis()
val messageIndex = adapter.lastPosition
val message = VkMessage( val message = VkMessage(
id = -1, id = Int.MAX_VALUE,
text = messageText, text = messageText,
isOut = true, isOut = true,
peerId = conversation.id, peerId = conversation.id,
@@ -304,10 +356,10 @@ class MessagesHistoryFragment :
replyMessage = attachmentController.message.value replyMessage = attachmentController.message.value
) )
adapter.add(message) adapter.add(message, beforeFooter = true, commitCallback = {
adapter.notifyItemInserted(adapter.actualSize - 1) binding.recyclerView.scrollToPosition(adapter.lastPosition)
binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
binding.message.clear() binding.message.clear()
})
val replyMessage = attachmentController.message.value val replyMessage = attachmentController.message.value
attachmentController.message.value = null attachmentController.message.value = null
@@ -316,8 +368,13 @@ class MessagesHistoryFragment :
peerId = conversation.id, peerId = conversation.id,
message = messageText, message = messageText,
randomId = 0, randomId = 0,
replyTo = replyMessage?.id replyTo = replyMessage?.id,
) { message.id = it } setId = { messageId ->
val messageToUpdate = adapter[messageIndex] as VkMessage
messageToUpdate.id = messageId
adapter[messageIndex] = messageToUpdate
}
)
} }
Action.EDIT -> { Action.EDIT -> {
val message = attachmentController.message.value ?: return val message = attachmentController.message.value ?: return
@@ -336,6 +393,7 @@ class MessagesHistoryFragment :
Action.DELETE -> attachmentController.message.value?.let { Action.DELETE -> attachmentController.message.value?.let {
showDeleteMessageDialog(it) showDeleteMessageDialog(it)
} }
else -> {}
} }
} }
@@ -346,12 +404,12 @@ class MessagesHistoryFragment :
is StartProgressEvent -> onProgressStarted() is StartProgressEvent -> onProgressStarted()
is StopProgressEvent -> onProgressStopped() is StopProgressEvent -> onProgressStopped()
is MessagesMarkAsImportant -> markMessagesAsImportant(event) is MessagesMarkAsImportantEvent -> markMessagesAsImportant(event)
is MessagesLoaded -> refreshMessages(event) is MessagesLoadedEvent -> refreshMessages(event)
is MessagesPin -> conversation.pinnedMessage = event.message is MessagesPinEvent -> conversation.pinnedMessage = event.message
is MessagesUnpin -> conversation.pinnedMessage = null is MessagesUnpinEvent -> conversation.pinnedMessage = null
is MessagesDelete -> deleteMessages(event) is MessagesDeleteEvent -> deleteMessages(event)
is MessagesEdit -> editMessage(event) is MessagesEditEvent -> editMessage(event)
} }
} }
@@ -395,26 +453,24 @@ class MessagesHistoryFragment :
} }
} }
private fun markMessagesAsImportant(event: MessagesMarkAsImportant) { private fun markMessagesAsImportant(event: MessagesMarkAsImportantEvent) {
var changed = false var changed = false
val positions = mutableListOf<Int>() val positions = mutableListOf<Int>()
for (i in adapter.values.indices) { for (i in adapter.indices) {
val message = adapter.values[i] val message = adapter[i] as VkMessage
message.important = event.important message.important = event.important
if (event.messagesIds.contains(message.id)) { if (event.messagesIds.contains(message.id)) {
if (!changed) changed = true if (!changed) changed = true
positions.add(i) positions.add(i)
adapter.values[i] = message adapter[i] = message
}
} }
} }
if (changed) positions.forEach { adapter.notifyItemChanged(it) } private fun refreshMessages(event: MessagesLoadedEvent) {
}
private fun refreshMessages(event: MessagesLoaded) {
adapter.profiles += event.profiles adapter.profiles += event.profiles
adapter.groups += event.groups adapter.groups += event.groups
@@ -424,22 +480,23 @@ class MessagesHistoryFragment :
private fun fillRecyclerView(values: List<VkMessage>) { private fun fillRecyclerView(values: List<VkMessage>) {
val smoothScroll = adapter.isNotEmpty() val smoothScroll = adapter.isNotEmpty()
adapter.values.clear() adapter.setItems(
adapter.values += values.sortedBy { it.date } values.sortedBy { it.date },
adapter.notifyItemRangeChanged(0, adapter.itemCount) withHeader = true,
withFooter = true,
commitCallback = {
if (smoothScroll) binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) if (smoothScroll) binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
else binding.recyclerView.scrollToPosition(adapter.lastPosition) else binding.recyclerView.scrollToPosition(adapter.lastPosition)
} }
)
}
private fun onItemClick(position: Int) { private fun onItemClick(position: Int) {
showOptionsDialog(position) showOptionsDialog(position)
} }
private fun onItemLongClick(position: Int) = true
private fun onAvatarLongClickListener(position: Int) { private fun onAvatarLongClickListener(position: Int) {
val message = adapter.values[position] val message = adapter[position] as VkMessage
val messageUser = VkUtils.getMessageUser(message, adapter.profiles) val messageUser = VkUtils.getMessageUser(message, adapter.profiles)
val messageGroup = VkUtils.getMessageGroup(message, adapter.groups) val messageGroup = VkUtils.getMessageGroup(message, adapter.groups)
@@ -449,7 +506,7 @@ class MessagesHistoryFragment :
} }
private fun showOptionsDialog(position: Int) { private fun showOptionsDialog(position: Int) {
val message = adapter.values[position] val message = adapter[position] as VkMessage
if (message.action != null) return if (message.action != null) return
val time = getString( val time = getString(
@@ -577,16 +634,14 @@ class MessagesHistoryFragment :
.show() .show()
} }
private fun deleteMessages(event: MessagesDelete) { private fun deleteMessages(event: MessagesDeleteEvent) {
adapter.removeMessagesByIds(event.messagesIds).let { val messagesToDelete = event.messagesIds.mapNotNull { id -> adapter.searchMessageById(id) }
it.forEach { index -> adapter.notifyItemRemoved(index) } adapter.removeAll(messagesToDelete)
}
} }
private fun editMessage(event: MessagesEdit) { private fun editMessage(event: MessagesEditEvent) {
adapter.searchMessageIndex(event.message.id)?.let { index -> adapter.searchMessageIndex(event.message.id)?.let { index ->
adapter.values[index] = event.message adapter[index] = event.message
adapter.notifyItemChanged(index)
} }
} }
@@ -610,8 +665,6 @@ class MessagesHistoryFragment :
} }
private fun applyMessage(message: VkMessage) { private fun applyMessage(message: VkMessage) {
showPanel()
val title = when { val title = when {
message.isGroup() && message.group.value != null -> message.group.value?.name message.isGroup() && message.group.value != null -> message.group.value?.name
message.isUser() && message.user.value != null -> message.user.value?.fullName message.isUser() && message.user.value != null -> message.user.value?.fullName
@@ -637,6 +690,8 @@ class MessagesHistoryFragment :
if (isEditing) { if (isEditing) {
binding.message.setText(message.text) binding.message.setText(message.text)
} }
showPanel()
} }
private fun clearMessage() { 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) if (attachmentController.isPanelVisible.value == false)
attachmentController.isPanelVisible.value = true attachmentController.isPanelVisible.value = true
binding.attachmentPanel.animate() val measuredHeight = binding.attachmentPanel.measuredHeight
.translationY(0f)
.alpha(1f) binding.attachmentPanel.updateLayoutParams<CoordinatorLayout.LayoutParams> {
.setDuration(duration) height = 0
.withStartAction { binding.attachmentPanel.isVisible = true }
.start()
} }
private fun hidePanel(duration: Long = 250) { binding.attachmentPanel.animate()
.translationY(0f)
.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() {
if (attachmentController.isPanelVisible.value == true) if (attachmentController.isPanelVisible.value == true)
attachmentController.isPanelVisible.value = false attachmentController.isPanelVisible.value = false
val currentHeight = binding.attachmentPanel.height
binding.attachmentPanel.animate() binding.attachmentPanel.animate()
.alpha(0f) .translationY(75F)
.translationY(50f) .setDuration(ATTACHMENT_PANEL_ANIMATION_DURATION)
.setDuration(duration)
.withEndAction { binding.attachmentPanel.isVisible = false }
.start() .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 package com.meloda.fast.screens.messages
import androidx.lifecycle.viewModelScope 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.VKConstants
import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
@@ -16,12 +18,25 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MessagesHistoryViewModel @Inject constructor( class MessagesHistoryViewModel @Inject constructor(
private val messages: MessagesDataSource private val messages: MessagesDataSource,
updatesParser: LongPollUpdatesParser
) : BaseViewModel() { ) : BaseViewModel() {
fun loadHistory( init {
peerId: Int updatesParser.onNewMessage {
) = viewModelScope.launch { // 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({ makeJob({
messages.getHistory( messages.getHistory(
MessagesGetHistoryRequest( MessagesGetHistoryRequest(
@@ -66,7 +81,7 @@ class MessagesHistoryViewModel @Inject constructor(
} }
sendEvent( sendEvent(
MessagesLoaded( MessagesLoadedEvent(
count = response.count, count = response.count,
profiles = profiles, profiles = profiles,
groups = groups, groups = groups,
@@ -116,7 +131,7 @@ class MessagesHistoryViewModel @Inject constructor(
onAnswer = { onAnswer = {
val response = it.response ?: return@makeJob val response = it.response ?: return@makeJob
sendEvent( sendEvent(
MessagesMarkAsImportant( MessagesMarkAsImportantEvent(
messagesIds = response, messagesIds = response,
important = important important = important
) )
@@ -142,14 +157,14 @@ class MessagesHistoryViewModel @Inject constructor(
}, },
onAnswer = { onAnswer = {
val response = it.response ?: return@makeJob val response = it.response ?: return@makeJob
sendEvent(MessagesPin(response.asVkMessage())) sendEvent(MessagesPinEvent(response.asVkMessage()))
} }
) )
} else { } else {
makeJob({ messages.unpin(MessagesUnPinMessageRequest(peerId = peerId)) }, makeJob({ messages.unpin(MessagesUnPinMessageRequest(peerId = peerId)) },
onAnswer = { onAnswer = {
println("Fast::MessagesHistoryViewModel::unPin::Response::${it.response}") println("Fast::MessagesHistoryViewModel::unPin::Response::${it.response}")
sendEvent(MessagesUnpin) sendEvent(MessagesUnpinEvent)
} }
) )
} }
@@ -172,7 +187,7 @@ class MessagesHistoryViewModel @Inject constructor(
deleteForAll = deleteForAll deleteForAll = deleteForAll
) )
) )
}, onAnswer = { sendEvent(MessagesDelete(messagesIds = messagesIds ?: listOf())) }) }, onAnswer = { sendEvent(MessagesDeleteEvent(messagesIds = messagesIds ?: emptyList())) })
} }
fun editMessage( fun editMessage(
@@ -195,13 +210,13 @@ class MessagesHistoryViewModel @Inject constructor(
}, },
onAnswer = { onAnswer = {
originalMessage.text = message originalMessage.text = message
sendEvent(MessagesEdit(originalMessage)) sendEvent(com.meloda.fast.screens.messages.MessagesEditEvent(originalMessage))
} }
) )
} }
} }
data class MessagesLoaded( data class MessagesLoadedEvent(
val count: Int, val count: Int,
val conversations: HashMap<Int, VkConversation>, val conversations: HashMap<Int, VkConversation>,
val messages: List<VkMessage>, val messages: List<VkMessage>,
@@ -209,21 +224,12 @@ data class MessagesLoaded(
val groups: HashMap<Int, VkGroup> val groups: HashMap<Int, VkGroup>
) : VkEvent() ) : VkEvent()
data class MessagesMarkAsImportant( data class MessagesMarkAsImportantEvent(val messagesIds: List<Int>, val important: Boolean) : VkEvent()
val messagesIds: List<Int>,
val important: Boolean
) : VkEvent()
data class MessagesPin( data class MessagesPinEvent(val message: VkMessage) : VkEvent()
val message: VkMessage
) : VkEvent()
object MessagesUnpin : VkEvent() object MessagesUnpinEvent : VkEvent()
data class MessagesDelete( data class MessagesDeleteEvent(val messagesIds: List<Int>) : VkEvent()
val messagesIds: List<Int>
) : VkEvent()
data class MessagesEdit( data class MessagesEditEvent(val message: VkMessage) : VkEvent()
val message: VkMessage
) : VkEvent()
@@ -12,6 +12,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.load import coil.load
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.VkUtils import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
@@ -19,6 +20,9 @@ import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.attachments.VkSticker import com.meloda.fast.api.model.attachments.VkSticker
import com.meloda.fast.common.AppGlobal import com.meloda.fast.common.AppGlobal
import com.meloda.fast.extensions.gone
import com.meloda.fast.extensions.toggleVisibility
import com.meloda.fast.extensions.visible
import com.meloda.fast.widget.BoundedLinearLayout import com.meloda.fast.widget.BoundedLinearLayout
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -150,9 +154,7 @@ class MessagesPreparator constructor(
} }
private fun prepareUnreadIndicator() { private fun prepareUnreadIndicator() {
if (unread != null) { unread?.toggleVisibility(!message.isRead(conversation))
unread.isVisible = message.isRead(conversation)
}
} }
private fun prepareSpacer() { private fun prepareSpacer() {
@@ -160,12 +162,20 @@ class MessagesPreparator constructor(
} }
private fun prepareAttachments() { private fun prepareAttachments() {
attachmentContainer?.removeAllViews()
textContainer?.let { textContainer ->
if (textContainer.childCount > 1) {
textContainer.removeViews(1, textContainer.childCount - 1)
}
}
if (attachmentContainer != null && textContainer != null) { if (attachmentContainer != null && textContainer != null) {
if (message.attachments.isNullOrEmpty()) { if (message.attachments.isNullOrEmpty()) {
attachmentContainer.isVisible = false attachmentContainer.gone()
attachmentContainer.removeAllViews()
} else { } else {
attachmentContainer.isVisible = true attachmentContainer.visible()
AttachmentInflater( AttachmentInflater(
context = context, context = context,
@@ -208,11 +218,23 @@ class MessagesPreparator constructor(
private fun prepareText() { private fun prepareText() {
if (bubble != null && text != null) { if (bubble != null && text != null) {
if (message.text == null) { if (message.text == null) {
text.isVisible = false text.gone()
bubble.isVisible = !message.attachments.isNullOrEmpty()
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 { } else {
text.isVisible = true text.visible()
bubble.isVisible = true bubble.visible()
text.text = VkUtils.prepareMessageText(message.text ?: "") 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.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.util.DisplayMetrics
import android.util.TypedValue import android.util.TypedValue
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import com.meloda.fast.common.AppGlobal import com.meloda.fast.common.AppGlobal
@@ -12,22 +11,8 @@ import com.meloda.fast.common.AppGlobal
object AndroidUtils { 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 { fun isDarkTheme(): Boolean {
val currentNightMode = return when (AppGlobal.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
AppGlobal.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return when (currentNightMode) {
Configuration.UI_MODE_NIGHT_YES -> true Configuration.UI_MODE_NIGHT_YES -> true
else -> false else -> false
} }
@@ -13,8 +13,7 @@ import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.util.AndroidUtils import com.meloda.fast.extensions.dpToPx
import kotlin.math.roundToInt
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class NoItemsView @JvmOverloads constructor( class NoItemsView @JvmOverloads constructor(
@@ -43,7 +42,7 @@ class NoItemsView @JvmOverloads constructor(
private fun create() { private fun create() {
val a = context.obtainStyledAttributes(attrs, R.styleable.NoItemsView) val a = context.obtainStyledAttributes(attrs, R.styleable.NoItemsView)
minimumWidth = AndroidUtils.px(256).roundToInt() minimumWidth = 256.dpToPx()
minimumHeight = minimumWidth minimumHeight = minimumWidth
orientation = VERTICAL orientation = VERTICAL
@@ -51,9 +50,12 @@ class NoItemsView @JvmOverloads constructor(
noItemsPicture = ImageView(context) noItemsPicture = ImageView(context)
val params = imageViewParams val imageViewSize = 64.dpToPx()
params.height = AndroidUtils.px(64).roundToInt()
params.width = AndroidUtils.px(64).roundToInt() val params = imageViewParams.apply {
height = imageViewSize
width = imageViewSize
}
noItemsPicture.layoutParams = params noItemsPicture.layoutParams = params
@@ -72,10 +74,10 @@ class NoItemsView @JvmOverloads constructor(
noItemsTextView = TextView(context) noItemsTextView = TextView(context)
val textParams = textViewParams val textParams = textViewParams
textParams.width = AndroidUtils.px(256).roundToInt() textParams.width = 256.dpToPx()
if (noItemsDrawable != null) { if (noItemsDrawable != null) {
textParams.topMargin = AndroidUtils.px(8).roundToInt() textParams.topMargin = 8.dpToPx()
} }
noItemsTextView.layoutParams = textParams 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 <stroke
android:width="2dp" android:width="2dp"
android:color="@color/messageOutStrokeColor" /> android:color="?colorSurfaceVariant" />
<solid android:color="@color/messageOutColor" /> <solid android:color="?colorSurface" />
<corners <corners
android:bottomLeftRadius="40dp" android:bottomLeftRadius="40dp"
@@ -4,9 +4,9 @@
<stroke <stroke
android:width="2dp" android:width="2dp"
android:color="@color/messageOutStrokeColor" /> android:color="?colorSurfaceVariant" />
<solid android:color="@color/messageOutColor" /> <solid android:color="?colorSurface" />
<corners <corners
android:bottomLeftRadius="40dp" android:bottomLeftRadius="40dp"
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/messageOutStrokeColor" /> <solid android:color="?colorSurfaceVariant" />
<corners <corners
android:bottomLeftRadius="40dp" android:bottomLeftRadius="40dp"
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/messageOutStrokeColor" /> <solid android:color="?colorSurfaceVariant" />
<corners <corners
android:bottomLeftRadius="40dp" android:bottomLeftRadius="40dp"
+1 -1
View File
@@ -9,7 +9,7 @@
<corners android:radius="50dp" /> <corners android:radius="50dp" />
<stroke <stroke
android:width="2dp" android:width="2dp"
android:color="?android:windowBackground" /> android:color="?colorBackground" />
</shape> </shape>
</item> </item>
</layer-list> </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>
+3 -19
View File
@@ -1,21 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/root_fragment_container"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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>
+2 -8
View File
@@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
@@ -76,9 +74,5 @@
android:backgroundTint="@color/a3_200" android:backgroundTint="@color/a3_200"
android:text="@android:string/ok" android:text="@android:string/ok"
app:elevation="0dp" /> app:elevation="0dp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</layout>
@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
@@ -16,7 +14,4 @@
android:paddingEnd="12dp" android:paddingEnd="12dp"
android:text="@string/message_delete_for_all" android:text="@string/message_delete_for_all"
app:useMaterialThemeColors="true" /> app:useMaterialThemeColors="true" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
@@ -69,9 +67,5 @@
android:backgroundTint="@color/a3_200" android:backgroundTint="@color/a3_200"
android:text="@android:string/ok" android:text="@android:string/ok"
app:elevation="0dp" /> app:elevation="0dp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</layout>
@@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -13,31 +11,25 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:elevation="0dp"> app:elevation="0dp">
<com.google.android.material.appbar.CollapsingToolbarLayout <androidx.appcompat.widget.Toolbar
android:id="@+id/collapsingToolbarLayout" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="?actionBarSize"
android:background="?colorBackground"
android:elevation="0dp" android:elevation="0dp"
app:collapsedTitleTextAppearance="@style/CollapsingToolbarCollapsedTitle" app:layout_scrollFlags="scroll|enterAlways|snap"
app:expandedTitleTextAppearance="@style/CollapsingToolbarTitle" app:menu="@menu/fragment_conversations"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" app:title="@string/title_messages"
app:title="Messages"> app:titleTextColor="?colorOnBackground">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/expandedImage"
android:layout_width="match_parent"
android:layout_height="140dp"
android:elevation="0dp" />
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/avatarContainer" android:id="@+id/avatarContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="end|center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="0dp" android:paddingStart="0dp"
android:paddingEnd="30dp" android:paddingEnd="16dp"
android:paddingBottom="30dp"
app:layout_collapseMode="none"> app:layout_collapseMode="none">
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
@@ -47,7 +39,7 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:background="?selectableItemBackgroundBorderless" android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_search" android:src="@drawable/ic_search"
android:tint="?colorSecondary3Variant" /> android:tint="?colorPrimary" />
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
android:id="@+id/avatar" android:id="@+id/avatar"
@@ -56,47 +48,7 @@
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.Toolbar>
<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> </com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@@ -126,8 +78,8 @@
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/createChat" android:id="@+id/createChat"
android:layout_width="56dp" android:layout_width="wrap_content"
android:layout_height="56dp" android:layout_height="wrap_content"
android:layout_gravity="end|bottom" android:layout_gravity="end|bottom"
android:layout_margin="16dp" android:layout_margin="16dp"
android:src="@drawable/ic_baseline_create_24" android:src="@drawable/ic_baseline_create_24"
@@ -137,6 +89,4 @@
app:pressedTranslationZ="1dp" app:pressedTranslationZ="1dp"
app:shapeAppearanceOverlay="@style/RoundedView.56" app:shapeAppearanceOverlay="@style/RoundedView.56"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
+4 -11
View File
@@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/loginRoot" android:id="@+id/loginRoot"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -11,10 +9,8 @@
<WebView <WebView
android:id="@+id/webView" android:id="@+id/webView"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:clickable="false"
android:focusable="false"
android:visibility="gone" /> android:visibility="gone" />
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
@@ -140,9 +136,6 @@
app:elevation="16dp" app:elevation="16dp"
app:icon="@drawable/ic_arrow_end" app:icon="@drawable/ic_arrow_end"
app:iconGravity="end" /> app:iconGravity="end" />
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout>
-27
View File
@@ -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,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -29,20 +27,31 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
android:background="@drawable/ic_messages_history_toolbar_gradient_background" android:background="@drawable/ic_messages_history_toolbar_gradient_background"
android:backgroundTint="@color/n1_50" android:backgroundTint="?colorBackground"
android:minHeight="140dp"> android:minHeight="140dp">
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingHorizontal="30dp" android:paddingStart="12dp"
android:paddingTop="18dp" android:paddingTop="18dp"
android:paddingEnd="30dp"
android:paddingBottom="24dp"> 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 <FrameLayout
android:layout_width="56dp" android:layout_width="48dp"
android:layout_height="56dp"> android:layout_height="48dp">
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
android:id="@+id/avatar" android:id="@+id/avatar"
@@ -55,107 +64,82 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.meloda.fast.widget.CircleImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/placeholderBack" android:id="@+id/placeholderBack"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:src="@color/n1_50" /> android:layout_margin="1dp"
tools:src="@color/colorOnUserAvatarAction" />
<com.meloda.fast.widget.CircleImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/placeholder" android:id="@+id/placeholder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:src="@drawable/ic_account_circle_cut" android:src="@drawable/ic_account_circle_cut"
app:tint="@color/n2_500" /> app:tint="@color/colorUserAvatarAction" />
</FrameLayout> </FrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/online" android:id="@+id/online"
android:layout_width="20dp" android:layout_width="14dp"
android:layout_height="20dp" android:layout_height="14dp"
android:layout_gravity="end|bottom" android:layout_gravity="end|bottom"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
android:layout_width="20dp" android:id="@+id/online_border"
android:layout_height="20dp" android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@color/n1_50" /> android:src="?colorBackground" />
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
android:layout_width="14dp" android:layout_width="10dp"
android:layout_height="14dp" android:layout_height="10dp"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_online_pc" android:src="@drawable/ic_online_pc"
app:tint="?colorSecondary2" /> 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/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>
<FrameLayout <FrameLayout
android:id="@+id/service" android:id="@+id/service"
android:layout_width="20dp" android:layout_width="16dp"
android:layout_height="20dp" android:layout_height="16dp"
android:layout_gravity="end|top" android:layout_gravity="end|top"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
android:layout_width="18dp" android:layout_width="14dp"
android:layout_height="18dp" android:layout_height="14dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@drawable/ic_back" android:background="@drawable/ic_back"
android:backgroundTint="@color/n2_500" android:backgroundTint="@color/colorUserAvatarAction"
android:elevation="0.5dp" /> android:elevation="0.5dp" />
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
android:id="@+id/phantomIcon" android:id="@+id/phantomIcon"
android:layout_width="14dp" android:layout_width="10dp"
android:layout_height="14dp" android:layout_height="10dp"
android:layout_gravity="center" android:layout_gravity="center"
android:elevation="1dp" android:elevation="1dp"
android:src="@drawable/ic_phantom" android:src="@drawable/ic_phantom"
android:visibility="gone" android:visibility="gone"
app:tint="@color/n2_10" app:tint="@color/colorOnUserAvatarAction"
tools:visibility="visible" /> tools:visibility="gone" />
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
android:id="@+id/callIcon" android:id="@+id/callIcon"
android:layout_width="14dp" android:layout_width="10dp"
android:layout_height="14dp" android:layout_height="10dp"
android:layout_gravity="center" android:layout_gravity="center"
android:elevation="1dp" android:elevation="1dp"
android:src="@drawable/ic_attachment_group_call" android:src="@drawable/ic_attachment_group_call"
android:visibility="gone" android:visibility="gone"
app:tint="@color/n2_0" /> app:tint="@color/colorOnUserAvatarAction"
tools:visibility="visible" />
</FrameLayout> </FrameLayout>
</FrameLayout> </FrameLayout>
@@ -170,9 +154,10 @@
android:id="@+id/title" android:id="@+id/title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/google_sans_regular"
android:maxLines="1" android:maxLines="1"
android:textColor="@color/n1_900" android:textColor="?colorOnBackground"
android:textSize="24sp" android:textSize="20sp"
tools:text="@tools:sample/full_names" /> tools:text="@tools:sample/full_names" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
@@ -181,7 +166,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:alpha="0.7" android:alpha="0.7"
android:maxLines="1" android:maxLines="1"
android:textColor="@color/n1_900" android:textColor="?colorOnBackground"
tools:text="Online" /> tools:text="Online" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
@@ -193,14 +178,16 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom" android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="30dp" android:layout_marginBottom="24dp"
android:elevation="2dp" android:elevation="2dp"
android:enabled="false" android:enabled="false"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
android:paddingVertical="4dp" android:paddingVertical="4dp"
android:textColor="@color/n1_900" android:textColor="?colorOnBackground"
android:visibility="gone" android:visibility="gone"
app:chipBackgroundColor="@color/n1_100" app:chipBackgroundColor="?colorBackgroundVariant"
app:chipCornerRadius="16dp"
app:chipStrokeWidth="0dp"
app:textEndPadding="12dp" app:textEndPadding="12dp"
app:textStartPadding="12dp" app:textStartPadding="12dp"
tools:text="today" tools:text="today"
@@ -215,14 +202,16 @@
android:layout_marginHorizontal="12dp" android:layout_marginHorizontal="12dp"
android:layout_marginBottom="35dp" android:layout_marginBottom="35dp"
android:background="@drawable/ic_chat_attachment_panel_background" android:background="@drawable/ic_chat_attachment_panel_background"
android:backgroundTint="@color/n2_100" android:backgroundTint="?colorSurfaceVariant"
android:minHeight="105dp" android:minHeight="105dp"
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp" android:padding="16dp"
android:translationY="50dp"
android:visibility="gone" android:visibility="gone"
app:layout_anchor="@+id/messagePanel" app:layout_anchor="@+id/messagePanel"
app:layout_anchorGravity="center_vertical|top" app:layout_anchorGravity="center_vertical|top"
tools:visibility="visible"> tools:translationY="0dp"
tools:visibility="gone">
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/replyMessage" android:id="@+id/replyMessage"
@@ -243,7 +232,7 @@
android:layout_weight="1" android:layout_weight="1"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="?textColorPrimary" android:textColor="?colorOnBackground"
app:fontFamily="@font/google_sans_regular" app:fontFamily="@font/google_sans_regular"
tools:text="Michael Bae" /> tools:text="Michael Bae" />
@@ -253,7 +242,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless" android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_round_close_20" android:src="@drawable/ic_round_close_20"
android:tint="@color/n1_800" /> android:tint="?colorOnBackground" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
@@ -263,7 +252,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="?textColorPrimary" android:textColor="?colorOnBackground"
android:textSize="16sp" android:textSize="16sp"
app:fontFamily="@font/roboto_regular" app:fontFamily="@font/roboto_regular"
tools:text="Short Message." /> tools:text="Short Message." />
@@ -274,10 +263,10 @@
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="40dp" android:layout_height="50dp"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:background="@drawable/ic_message_panel_gradient" android:background="@drawable/ic_message_panel_gradient"
android:backgroundTint="@color/n1_50" /> android:backgroundTint="?colorBackground" />
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/messagePanel" android:id="@+id/messagePanel"
@@ -286,7 +275,7 @@
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:layout_margin="12dp" android:layout_margin="12dp"
android:background="@drawable/ic_message_panel_background" android:background="@drawable/ic_message_panel_background"
android:backgroundTint="?colorSurface" android:backgroundTint="@color/colorSurface"
android:clickable="true" android:clickable="true"
android:elevation="3dp" android:elevation="3dp"
android:focusable="true" android:focusable="true"
@@ -301,8 +290,11 @@
android:layout_marginHorizontal="20dp" android:layout_marginHorizontal="20dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:gravity="center_vertical"
android:hint="@string/message_input_hint" android:hint="@string/message_input_hint"
android:singleLine="true" /> android:maxLines="3"
android:textColor="?colorOnBackground"
android:textColorHint="@color/colorOnBackground50" />
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/attach" android:id="@+id/attach"
@@ -312,7 +304,7 @@
android:layout_marginEnd="18dp" android:layout_marginEnd="18dp"
android:background="?selectableItemBackgroundBorderless" android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_attach_file_24" android:src="@drawable/ic_baseline_attach_file_24"
android:tint="?colorSecondary3" /> android:tint="?colorPrimary" />
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/action" android:id="@+id/action"
@@ -322,7 +314,7 @@
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
android:background="?selectableItemBackgroundBorderless" android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_round_mic_24" android:src="@drawable/ic_round_mic_24"
android:tint="?colorSecondary3" /> android:tint="?colorPrimary" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
@@ -333,7 +325,4 @@
android:layout_gravity="center" android:layout_gravity="center"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
+30 -32
View File
@@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
<FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginVertical="4dp" android:layout_marginVertical="4dp"
@@ -14,7 +12,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="20dp" android:layout_marginStart="20dp"
android:backgroundTint="@color/n1_100" android:backgroundTint="?colorBackgroundVariant"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingVertical="8dp" android:paddingVertical="8dp"
android:paddingStart="8dp" android:paddingStart="8dp"
@@ -36,19 +34,19 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.meloda.fast.widget.CircleImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/placeholderBack" android:id="@+id/placeholderBack"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:src="@color/n1_50" /> android:layout_margin="1dp"
tools:src="@color/colorOnUserAvatarAction" />
<com.meloda.fast.widget.CircleImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/placeholder" android:id="@+id/placeholder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:src="@drawable/ic_account_circle_cut" android:src="@drawable/ic_account_circle_cut"
app:tint="@color/n2_500" /> app:tint="@color/colorUserAvatarAction" />
</FrameLayout> </FrameLayout>
<FrameLayout <FrameLayout
@@ -60,17 +58,18 @@
tools:visibility="visible"> tools:visibility="visible">
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
android:id="@+id/online_border"
android:layout_width="20dp" android:layout_width="20dp"
android:layout_height="20dp" android:layout_height="20dp"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@color/n1_50" /> android:src="?colorBackground" />
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
android:layout_width="14dp" android:layout_width="14dp"
android:layout_height="14dp" android:layout_height="14dp"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_online_pc" android:src="@drawable/ic_online_pc"
app:tint="?colorSecondary2" /> android:tint="?colorPrimaryVariant" />
</FrameLayout> </FrameLayout>
@@ -87,7 +86,7 @@
android:layout_height="18dp" android:layout_height="18dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@drawable/ic_back" android:background="@drawable/ic_back"
android:backgroundTint="@color/n2_500" android:backgroundTint="@color/colorUserAvatarAction"
android:elevation="0.5dp" /> android:elevation="0.5dp" />
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
@@ -97,7 +96,8 @@
android:layout_gravity="center" android:layout_gravity="center"
android:elevation="1dp" android:elevation="1dp"
android:src="@drawable/ic_round_push_pin_24" android:src="@drawable/ic_round_push_pin_24"
app:tint="@color/n2_0" />
app:tint="@color/colorOnUserAvatarAction" />
</FrameLayout> </FrameLayout>
@@ -114,7 +114,7 @@
android:layout_height="18dp" android:layout_height="18dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@drawable/ic_back" android:background="@drawable/ic_back"
android:backgroundTint="@color/n2_500" android:backgroundTint="@color/colorUserAvatarAction"
android:elevation="0.5dp" /> android:elevation="0.5dp" />
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
@@ -125,8 +125,8 @@
android:elevation="1dp" android:elevation="1dp"
android:src="@drawable/ic_phantom" android:src="@drawable/ic_phantom"
android:visibility="gone" android:visibility="gone"
app:tint="@color/n2_10" app:tint="@color/colorOnUserAvatarAction"
tools:visibility="visible" /> tools:visibility="gone" />
<com.meloda.fast.widget.CircleImageView <com.meloda.fast.widget.CircleImageView
android:id="@+id/callIcon" android:id="@+id/callIcon"
@@ -136,7 +136,8 @@
android:elevation="1dp" android:elevation="1dp"
android:src="@drawable/ic_attachment_group_call" android:src="@drawable/ic_attachment_group_call"
android:visibility="gone" android:visibility="gone"
app:tint="@color/n2_0" /> app:tint="@color/colorOnUserAvatarAction"
tools:visibility="visible" />
</FrameLayout> </FrameLayout>
</FrameLayout> </FrameLayout>
@@ -165,8 +166,8 @@
android:layout_weight="1" android:layout_weight="1"
android:fontFamily="@font/google_sans_regular" android:fontFamily="@font/google_sans_regular"
android:maxLines="2" android:maxLines="2"
android:textColor="?textColorPrimary" android:textColor="?colorOnBackground"
android:textSize="22sp" android:textSize="20sp"
tools:text="Title" /> tools:text="Title" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
@@ -178,11 +179,12 @@
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:layout_weight="0" android:layout_weight="0"
android:background="@drawable/ic_back" android:background="@drawable/ic_back"
android:backgroundTint="?colorSecondary3" android:backgroundTint="?colorOnBackgroundVariantContainer"
android:gravity="center" android:gravity="center"
android:minWidth="18dp" android:minWidth="18dp"
android:textColor="?colorOnSecondary3" android:paddingHorizontal="2dp"
android:textSize="10sp" android:textColor="?colorOnBackgroundVariantOnContainer"
android:textSize="11sp"
android:visibility="gone" android:visibility="gone"
tools:text="12" tools:text="12"
tools:visibility="visible" /> tools:visibility="visible" />
@@ -199,7 +201,7 @@
android:alpha="0.5" android:alpha="0.5"
android:fontFamily="@font/roboto_regular" android:fontFamily="@font/roboto_regular"
android:gravity="end|center_vertical" android:gravity="end|center_vertical"
android:textColor="?textColorSecondaryVariant" android:textColor="?colorOutline"
tools:text="20:00" /> tools:text="20:00" />
</RelativeLayout> </RelativeLayout>
@@ -226,7 +228,7 @@
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:src="@drawable/ic_baseline_attach_file_24" android:src="@drawable/ic_baseline_attach_file_24"
android:visibility="gone" android:visibility="gone"
app:tint="?textColorSecondaryVariant" app:tint="?colorOutline"
tools:visibility="visible" /> tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
@@ -236,20 +238,16 @@
android:alpha="0.7" android:alpha="0.7"
android:fontFamily="@font/roboto_regular" android:fontFamily="@font/roboto_regular"
android:maxLines="2" android:maxLines="2"
android:textColor="?textColorPrimary" android:textColor="?colorOnBackground"
android:textSize="16sp" android:textSize="16sp"
tools:text="Message" /> tools:text="Message" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?selectableItemBackground" /> android:background="?selectableItemBackground"
tools:visibility="gone" />
</FrameLayout> </FrameLayout>
</layout>
@@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
@@ -15,7 +13,7 @@
android:layout_gravity="top" android:layout_gravity="top"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:background="@drawable/ic_play_button_circle_background" android:background="@drawable/ic_play_button_circle_background"
android:backgroundTint="@color/a3_200"> android:backgroundTint="?colorPrimaryVariant">
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -39,7 +37,7 @@
android:ellipsize="end" android:ellipsize="end"
android:fontFamily="@font/google_sans_regular" android:fontFamily="@font/google_sans_regular"
android:maxLines="1" android:maxLines="1"
android:textColor="@color/n1_800" android:textColor="?colorOnBackground"
android:textSize="18sp" android:textSize="18sp"
tools:text="Даня, дай Фаст" /> tools:text="Даня, дай Фаст" />
@@ -49,11 +47,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:alpha="0.8" android:alpha="0.8"
android:fontFamily="@font/roboto_regular" android:fontFamily="@font/roboto_regular"
android:textColor="@color/n1_800" android:textColor="?colorOnBackground"
tools:text="Эльчин Оруджев | 0:36" /> tools:text="Эльчин Оруджев | 0:36" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
@@ -15,7 +13,7 @@
android:layout_gravity="top" android:layout_gravity="top"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:background="@drawable/ic_play_button_circle_background" android:background="@drawable/ic_play_button_circle_background"
android:backgroundTint="@color/a3_200"> android:backgroundTint="?colorPrimaryVariant">
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -39,7 +37,7 @@
android:ellipsize="end" android:ellipsize="end"
android:fontFamily="@font/google_sans_regular" android:fontFamily="@font/google_sans_regular"
android:maxLines="1" android:maxLines="1"
android:textColor="@color/n1_800" android:textColor="?colorOnBackground"
android:textSize="18sp" android:textSize="18sp"
tools:text="Исходящий звонок" /> tools:text="Исходящий звонок" />
@@ -49,11 +47,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:alpha="0.8" android:alpha="0.8"
android:fontFamily="@font/roboto_regular" android:fontFamily="@font/roboto_regular"
android:textColor="@color/n1_800" android:textColor="?colorOnBackground"
tools:text="Отменён" /> tools:text="Отменён" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
@@ -15,7 +13,7 @@
android:layout_gravity="top" android:layout_gravity="top"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:background="@drawable/ic_play_button_circle_background" android:background="@drawable/ic_play_button_circle_background"
android:backgroundTint="@color/a3_200"> android:backgroundTint="?colorPrimaryVariant">
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -39,7 +37,7 @@
android:ellipsize="end" android:ellipsize="end"
android:fontFamily="@font/google_sans_regular" android:fontFamily="@font/google_sans_regular"
android:maxLines="1" android:maxLines="1"
android:textColor="@color/n1_800" android:textColor="?colorOnBackground"
android:textSize="18sp" android:textSize="18sp"
tools:text="Kids" /> tools:text="Kids" />
@@ -49,11 +47,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:alpha="0.8" android:alpha="0.8"
android:fontFamily="@font/roboto_regular" android:fontFamily="@font/roboto_regular"
android:textColor="@color/n1_800" android:textColor="?colorOnBackground"
tools:text="3.28 TB" /> tools:text="3.28 TB" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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"
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
@@ -10,7 +8,4 @@
android:id="@+id/image" android:id="@+id/image"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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"
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
@@ -10,7 +8,4 @@
android:id="@+id/image" android:id="@+id/image"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout>

Some files were not shown because too many files have changed in this diff Show More