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

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