Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ plugins {
}

android {
compileSdk 34
compileSdk 35
namespace "otus.gpb.recyclerview"

buildFeatures {
viewBinding true
}

defaultConfig {
applicationId "otus.gpb.recyclerview"
minSdk 26
targetSdk 34
targetSdk 35
versionCode 1
versionName "1.0"

Expand All @@ -34,11 +38,14 @@ android {

dependencies {

implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'com.github.bumptech.glide:glide:4.16.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
implementation('androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0')
implementation('androidx.lifecycle:lifecycle-runtime-ktx:2.9.0')
}
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand Down
62 changes: 61 additions & 1 deletion app/src/main/java/otus/gpb/recyclerview/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,72 @@
package otus.gpb.recyclerview

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch
import otus.gpb.recyclerview.data.ChatRepository
import otus.gpb.recyclerview.data.ChatViewModel
import otus.gpb.recyclerview.data.initDB
import otus.gpb.recyclerview.ui.ChatItemDecorator
import otus.gpb.recyclerview.ui.ChatItemTouchHelper
import otus.gpb.recyclerview.ui.ChatListAdapter

class MainActivity : AppCompatActivity() {

private lateinit var viewModel: ChatViewModel
private lateinit var adapter: ChatListAdapter
private lateinit var recyclerView: RecyclerView
private val repository = ChatRepository()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

initDB()

recyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
adapter = ChatListAdapter()

viewModel = ViewModelProvider(this, object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ChatViewModel(repository) as T
}
})[ChatViewModel::class.java]

lifecycleScope.launch {
viewModel.chats.collect { chats ->
adapter.submitList(chats)
}
}

recyclerView.addItemDecoration(ChatItemDecorator(this))
recyclerView.apply {
clipChildren = false
clipToPadding = false
}

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)

val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisibleChat = layoutManager.findLastVisibleItemPosition()
val chatsCount = layoutManager.itemCount

if (!viewModel.isLoading.value && lastVisibleChat >= chatsCount - 5) {
viewModel.loadMoreChats()
}
}
})

recyclerView.adapter = adapter
ItemTouchHelper(ChatItemTouchHelper(viewModel)).attachToRecyclerView(recyclerView)

}
}
69 changes: 69 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/data/Chat.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package otus.gpb.recyclerview.data

import java.time.Duration
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale

enum class MessageStatus {
READ, DELIVERED
}

enum class MessageDirection {
IN, OUT
}

data class Chat(
var id: Int,
val user: User, // user with whom the chat is held
val timeRead: Instant? = null, // Time when potentially the chat was seen/"read" status
val isScam: Boolean = false, // Automatically (or manually) marked as spam
val mutedUntil: Instant? = null, // Time until chat is muted, i.e. you won't get notifications from the chat;
val isMutedForever: Boolean = false,
var isArchived: Boolean, // The chat is archived, it won't be displayed in the main chat list
var isVoip: Boolean = false, // Has voice message (?)
var timeLast: Instant? = null,
var textLast: String = "",
var isMentionedLast: Boolean = false,
var statusLast: MessageStatus? = null,
var unreadCount: Int = 0,
var directionLast: MessageDirection? = null // for displaying checkers in front of the last message date

) {
fun isMuted(): Boolean {
return isMutedForever || (mutedUntil != null && mutedUntil.isAfter(Instant.now()))
}

fun getLastMessageTime(): String {
val result: String =
if (timeLast == null)
""
else {
val diff = Duration.between(timeLast, Instant.now()).abs()
val dateTimeWithTimeZone = timeLast!!.atZone(ZoneId.systemDefault())
DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM)
.withLocale(Locale.getDefault())
when {
diff.toHours() < 24 -> {
DateTimeFormatter.ofPattern("HH:mm").format(dateTimeWithTimeZone)
}

diff.toDays() < 7 -> {
DateTimeFormatter.ofPattern("EEE").format(dateTimeWithTimeZone)
}

diff.toDays() < 365 -> {
DateTimeFormatter.ofPattern("MMM dd").format(dateTimeWithTimeZone)
}

else -> {
DateTimeFormatter.ofPattern("dd.MM.yyyy").format(dateTimeWithTimeZone)
}
}
}
return result
}

}
40 changes: 40 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/data/ChatRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package otus.gpb.recyclerview.data

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update

class ChatRepository {
private val _allChats = MutableStateFlow(emptyList<Chat>())

private val _page = MutableStateFlow(0)

fun getChatsFilteredAndSorted(): StateFlow<List<Chat>> {
return _allChats.map { chats ->
chats.asSequence()
.filter { !it.isArchived }
.toList()
}.stateIn(CoroutineScope(Dispatchers.Default), SharingStarted.Eagerly, emptyList())
}

fun toggleArchiveChat(chatId: Int) {
_allChats.update { chats ->
chats.map { chat ->
if (chat.id == chatId) chat.copy(isArchived = !chat.isArchived) else chat
}
}
}

fun loadMoreChats() {
if (_allChats.value.size < getChatsCount()) {
_page.update { it + 1 }
_allChats.update { chats -> chats + loadMoreChatsFromDB(_page.value) }
}
}
}

Loading