srctree

Robin Linden parent f299497d 685681fe
Add an unread message indicator to the contact list

inlinesplit
atox/src/main/java/ltd/evilcorp/atox/tox/EventListenerCallbacks.kt added: 89, removed: 56, total 33
@@ -6,6 +6,7 @@ import ltd.evilcorp.core.repository.FriendRequestRepository
import ltd.evilcorp.core.repository.MessageRepository
import ltd.evilcorp.core.repository.UserRepository
import ltd.evilcorp.core.vo.*
import ltd.evilcorp.domain.feature.ChatManager
import ltd.evilcorp.domain.feature.FileTransferManager
import ltd.evilcorp.domain.tox.Tox
import ltd.evilcorp.domain.tox.ToxEventListener
@@ -21,6 +22,7 @@ class EventListenerCallbacks @Inject constructor(
private val friendRequestRepository: FriendRequestRepository,
private val messageRepository: MessageRepository,
private val userRepository: UserRepository,
private val chatManager: ChatManager,
private val fileTransferManager: FileTransferManager,
private val notificationHelper: NotificationHelper,
private val tox: Tox
@@ -60,13 +62,17 @@ class EventListenerCallbacks @Inject constructor(
}
}
 
friendMessageHandler = { publicKey, _, _, message ->
friendMessageHandler = { publicKey, _, _, msg ->
val timestamp = getDate()
contactRepository.setLastMessage(publicKey, timestamp)
messageRepository.add(
Message(publicKey, message, Sender.Received, Int.MIN_VALUE, timestamp)
Message(publicKey, msg, Sender.Received, Int.MIN_VALUE, timestamp)
)
notificationHelper.showMessageNotification(contactByPublicKey(publicKey), message)
 
if (chatManager.activeChat != publicKey) {
notificationHelper.showMessageNotification(contactByPublicKey(publicKey), msg)
contactRepository.setHasUnreadMessages(publicKey, true)
}
}
 
friendNameHandler = { publicKey, newName ->
 
atox/src/main/java/ltd/evilcorp/atox/ui/ContactAdapter.kt added: 89, removed: 56, total 33
@@ -90,6 +90,11 @@ class ContactAdapter(
}
vh.status.setColorFilter(colorByStatus(resources, this))
setAvatarFromContact(vh.image, this)
vh.unreadIndicator.visibility = if (hasUnreadMessages) {
View.VISIBLE
} else {
View.GONE
}
}
 
view
@@ -107,5 +112,6 @@ class ContactAdapter(
val lastMessage: TextView = row.lastMessage
val status: ImageView = row.statusIndicator
val image: ImageView = row.profileImage
val unreadIndicator: ImageView = row.unreadIndicator
}
}
 
atox/src/main/java/ltd/evilcorp/atox/ui/NotificationHelper.kt added: 89, removed: 56, total 33
@@ -13,6 +13,7 @@ import ltd.evilcorp.atox.R
import ltd.evilcorp.atox.ui.chat.CONTACT_PUBLIC_KEY
import ltd.evilcorp.core.vo.Contact
import ltd.evilcorp.core.vo.FriendRequest
import ltd.evilcorp.domain.tox.PublicKey
import javax.inject.Inject
import javax.inject.Singleton
 
@@ -29,15 +30,6 @@ class NotificationHelper @Inject constructor(
createNotificationChannel()
}
 
var activeChat = ""
set(value) {
if (value.isNotEmpty())
dismissNotifications(value)
field = value
}
 
private fun dismissNotifications(value: String) = notifier.cancel(value.hashCode())
 
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
@@ -60,9 +52,9 @@ class NotificationHelper @Inject constructor(
notifier.createNotificationChannels(listOf(messageChannel, friendRequestChannel))
}
 
fun showMessageNotification(contact: Contact, message: String) {
if (contact.publicKey == activeChat) return
fun dismissNotifications(publicKey: PublicKey) = notifier.cancel(publicKey.string().hashCode())
 
fun showMessageNotification(contact: Contact, message: String) {
val notificationBuilder = NotificationCompat.Builder(context, MESSAGE)
.setSmallIcon(android.R.drawable.sym_action_chat)
.setContentTitle(contact.name)
 
atox/src/main/java/ltd/evilcorp/atox/ui/chat/ChatViewModel.kt added: 89, removed: 56, total 33
@@ -24,12 +24,14 @@ class ChatViewModel @Inject constructor(
fun sendMessage(message: String) = chatManager.sendMessage(publicKey, message)
fun clearHistory() = chatManager.clearHistory(publicKey)
fun setActiveChat(pubKey: PublicKey) {
if (pubKey.string().isEmpty()) {
publicKey = pubKey
 
if (publicKey.string().isEmpty()) {
setTyping(false)
}
 
publicKey = pubKey
notificationHelper.activeChat = pubKey.string()
notificationHelper.dismissNotifications(publicKey)
chatManager.activeChat = publicKey.string()
}
 
fun setTyping(typing: Boolean) {
 
atox/src/main/res/layout/contact_list_view_item.xml added: 89, removed: 56, total 33
@@ -8,30 +8,19 @@
android:padding="10dp">
<include layout="@layout/profile_image_layout"/>
 
<LinearLayout android:id="@+id/topLine"
android:layout_width="0dp"
<TextView android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="@id/publicKey"
app:layout_constraintTop_toTopOf="parent"
tools:text="name goes here"/>
<TextView android:id="@+id/lastMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:orientation="horizontal"
android:weightSum="1"
app:layout_constraintLeft_toRightOf="@id/imageContainer"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight=".7"
android:gravity="start"
android:textStyle="bold"
tools:text="name goes here"/>
 
<TextView android:id="@+id/lastMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight=".3"
android:gravity="end"
tools:text="28 May 2019 11:49"/>
</LinearLayout>
app:layout_constraintTop_toTopOf="parent"
tools:text="28 May 2019 11:49"/>
 
<TextView android:id="@+id/publicKey"
android:layout_width="0dp"
@@ -40,7 +29,18 @@
android:ellipsize="end"
android:singleLine="true"
app:layout_constraintLeft_toRightOf="@id/imageContainer"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/topLine"
app:layout_constraintRight_toLeftOf="@id/unreadIndicator"
app:layout_constraintTop_toBottomOf="@id/name"
tools:text="PUBLIC KEY GOES HERE AND GOES ON AND ON AND ON AND ON"/>
<ImageView android:id="@+id/unreadIndicator"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:contentDescription="@null"
android:src="@drawable/circle"
android:tint="@color/colorPrimary"
android:translationZ="9dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/name"/>
</androidx.constraintlayout.widget.ConstraintLayout>
 
core/src/main/java/ltd/evilcorp/core/db/ContactDao.kt added: 89, removed: 56, total 33
@@ -50,4 +50,7 @@ internal interface ContactDao {
 
@Query("UPDATE contacts SET avatar_uri = :uri WHERE public_key = :publicKey")
fun setAvatarUri(publicKey: String, uri: String)
 
@Query("UPDATE contacts SET has_unread_messages = :anyUnread WHERE public_key = :publicKey")
fun setHasUnreadMessages(publicKey: String, anyUnread: Boolean)
}
 
core/src/main/java/ltd/evilcorp/core/db/Database.kt added: 89, removed: 56, total 33
@@ -7,7 +7,7 @@ import ltd.evilcorp.core.vo.*
 
@Database(
entities = [Contact::class, FileTransfer::class, FriendRequest::class, Message::class, User::class],
version = 1
version = 2
)
@TypeConverters(Converters::class)
abstract class Database : RoomDatabase() {
 
filename was Deleted added: 89, removed: 56, total 33
@@ -0,0 +1,10 @@
package ltd.evilcorp.core.db
 
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
 
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) = db.execSQL(
"ALTER TABLE contacts ADD COLUMN has_unread_messages INTEGER NOT NULL DEFAULT 0"
)
}
 
core/src/main/java/ltd/evilcorp/core/di/DatabaseModule.kt added: 89, removed: 56, total 33
@@ -14,7 +14,7 @@ class DatabaseModule {
@Provides
fun provideDatabase(application: Application): Database =
Room.databaseBuilder(application, Database::class.java, "core_db")
.fallbackToDestructiveMigration() // TODO(robinlinden): Delete this.
.addMigrations(MIGRATION_1_2)
.build()
 
@Singleton
 
core/src/main/java/ltd/evilcorp/core/repository/ContactRepository.kt added: 89, removed: 56, total 33
@@ -27,4 +27,5 @@ class ContactRepository @Inject internal constructor(
fun setConnectionStatus(publicKey: String, status: ConnectionStatus) = dao.setConnectionStatus(publicKey, status)
fun setTyping(publicKey: String, typing: Boolean) = dao.setTyping(publicKey, typing)
fun setAvatarUri(publicKey: String, uri: String) = dao.setAvatarUri(publicKey, uri)
fun setHasUnreadMessages(publicKey: String, anyUnread: Boolean) = dao.setHasUnreadMessages(publicKey, anyUnread)
}
 
core/src/main/java/ltd/evilcorp/core/vo/Contact.kt added: 89, removed: 56, total 33
@@ -42,5 +42,8 @@ data class Contact(
var typing: Boolean = false,
 
@ColumnInfo(name = "avatar_uri")
var avatarUri: String = ""
var avatarUri: String = "",
 
@ColumnInfo(name = "has_unread_messages")
var hasUnreadMessages: Boolean = false
)
 
domain/src/main/java/ltd/evilcorp/domain/feature/ChatManager.kt added: 89, removed: 56, total 33
@@ -11,12 +11,22 @@ import ltd.evilcorp.domain.tox.MAX_MESSAGE_LENGTH
import ltd.evilcorp.domain.tox.PublicKey
import ltd.evilcorp.domain.tox.Tox
import javax.inject.Inject
import javax.inject.Singleton
 
@Singleton
class ChatManager @Inject constructor(
private val contactRepository: ContactRepository,
private val messageRepository: MessageRepository,
private val tox: Tox
) : CoroutineScope by GlobalScope {
var activeChat = ""
set(value) {
field = value
if (value.isNotEmpty()) launch {
contactRepository.setHasUnreadMessages(value, false)
}
}
 
fun messagesFor(publicKey: PublicKey) = messageRepository.get(publicKey.string())
 
fun sendMessage(publicKey: PublicKey, message: String) = launch {