From d4a1192af79fbe057ffb3fee90caad5438b47f70 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Tue, 27 Mar 2018 17:44:42 +0200
Subject: [PATCH] Implements day change messages

---
 .../ui/chat/messages/MessageAdapter.kt        | 10 ++-
 .../ui/chat/messages/MessageListFragment.kt   | 12 ++-
 .../chat/messages/QuasselMessageRenderer.kt   | 38 ++++++---
 .../layout/widget_chatmessage_daychange.xml   | 36 +++++++++
 .../persistence/QuasselDatabase.kt            | 81 ++++++++++++++++++-
 5 files changed, 155 insertions(+), 22 deletions(-)
 create mode 100644 app/src/main/res/layout/widget_chatmessage_daychange.xml

diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt
index e1b60e6b4..5814c3932 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt
@@ -104,13 +104,15 @@ class MessageAdapter(
     expansionListener: ((QuasselDatabase.DatabaseMessage) -> Unit)? = null
   ) : RecyclerView.ViewHolder(itemView) {
     @BindView(R.id.time)
-    lateinit var time: TextView
+    @JvmField
+    var time: TextView? = null
 
     @BindView(R.id.content)
     lateinit var content: TextView
 
     @BindView(R.id.markerline)
-    lateinit var markerline: View
+    @JvmField
+    var markerline: View? = null
 
     private var message: FormattedMessage? = null
 
@@ -142,9 +144,9 @@ class MessageAdapter(
     fun bind(message: FormattedMessage) {
       this.message = message
 
-      time.text = message.time
+      time?.text = message.time
       content.text = message.content
-      markerline.visibleIf(message.isMarkerLine)
+      markerline?.visibleIf(message.isMarkerLine)
 
       this.itemView.isSelected = message.isSelected
     }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
index e6a55ceef..91ae8ed45 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
@@ -20,6 +20,7 @@ import de.kuschku.libquassel.quassel.syncables.BufferSyncer
 import de.kuschku.libquassel.util.helpers.value
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.persistence.QuasselDatabase
+import de.kuschku.quasseldroid.persistence.findByBufferIdPagedWithDayChange
 import de.kuschku.quasseldroid.settings.AppearanceSettings
 import de.kuschku.quasseldroid.settings.BacklogSettings
 import de.kuschku.quasseldroid.util.helper.*
@@ -126,8 +127,7 @@ class MessageListFragment : ServiceBoundFragment() {
   private val boundaryCallback = object :
     PagedList.BoundaryCallback<DisplayMessage>() {
     override fun onItemAtFrontLoaded(itemAtFront: DisplayMessage) = Unit
-    override fun onItemAtEndLoaded(itemAtEnd: DisplayMessage) =
-      loadMore(lastMessageId = itemAtEnd.content.messageId)
+    override fun onItemAtEndLoaded(itemAtEnd: DisplayMessage) = loadMore()
   }
 
   override fun onCreateView(
@@ -162,6 +162,9 @@ class MessageListFragment : ServiceBoundFragment() {
     messageList.layoutManager = linearLayoutManager
     messageList.itemAnimator = null
     messageList.setItemViewCacheSize(20)
+    messageList.addItemDecoration(object : RecyclerView.ItemDecoration() {
+
+    })
 
     var isScrolling = false
     messageList.addOnScrollListener(
@@ -189,8 +192,9 @@ class MessageListFragment : ServiceBoundFragment() {
                              viewModel.markerLine)
       .toLiveData().switchMapNotNull { (buffer, selected, expanded, markerLine) ->
         database.filtered().listen(accountId, buffer).switchMapNotNull { filtered ->
+
           LivePagedListBuilder(
-            database.message().findByBufferIdPaged(buffer, filtered).map {
+            database.message().findByBufferIdPagedWithDayChange(buffer, filtered).map {
               DisplayMessage(
                 content = it,
                 isSelected = selected.contains(it.messageId),
@@ -307,4 +311,4 @@ class MessageListFragment : ServiceBoundFragment() {
     }
   }
 
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt
index 5aff45b92..597b8246c 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt
@@ -26,6 +26,7 @@ import de.kuschku.quasseldroid.viewmodel.data.FormattedMessage
 import org.intellij.lang.annotations.Language
 import org.threeten.bp.ZoneId
 import org.threeten.bp.format.DateTimeFormatter
+import org.threeten.bp.format.FormatStyle
 import javax.inject.Inject
 
 class QuasselMessageRenderer @Inject constructor(
@@ -36,6 +37,8 @@ class QuasselMessageRenderer @Inject constructor(
     timePattern(appearanceSettings.showSeconds, appearanceSettings.use24hClock)
   )
 
+  private val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
+
   val monospaceItalic = Typeface.create(Typeface.MONOSPACE, Typeface.ITALIC)
 
   private fun timePattern(showSeconds: Boolean,
@@ -52,14 +55,15 @@ class QuasselMessageRenderer @Inject constructor(
   private val zoneId = ZoneId.systemDefault()
 
   override fun layout(type: Message_Type?, hasHighlight: Boolean) = when (type) {
-    Notice -> R.layout.widget_chatmessage_notice
-    Server -> R.layout.widget_chatmessage_server
-    Error  -> R.layout.widget_chatmessage_error
-    Action -> R.layout.widget_chatmessage_action
-    Plain  -> R.layout.widget_chatmessage_plain
-    Nick, Mode, Join, Part, Quit, Kick, Kill, Info, DayChange, Topic, NetsplitJoin, NetsplitQuit,
-    Invite -> R.layout.widget_chatmessage_info
-    else   -> R.layout.widget_chatmessage_placeholder
+    Notice    -> R.layout.widget_chatmessage_notice
+    Server    -> R.layout.widget_chatmessage_server
+    Error     -> R.layout.widget_chatmessage_error
+    Action    -> R.layout.widget_chatmessage_action
+    Plain     -> R.layout.widget_chatmessage_plain
+    Nick, Mode, Join, Part, Quit, Kick, Kill, Info, Topic, NetsplitJoin, NetsplitQuit,
+    Invite    -> R.layout.widget_chatmessage_info
+    DayChange -> R.layout.widget_chatmessage_daychange
+    else      -> R.layout.widget_chatmessage_placeholder
   }
 
   override fun init(viewHolder: MessageAdapter.QuasselMessageViewHolder,
@@ -69,7 +73,7 @@ class QuasselMessageRenderer @Inject constructor(
       viewHolder.itemView.context.theme.styledAttributes(
         R.attr.colorForegroundHighlight, R.attr.colorBackgroundHighlight
       ) {
-        viewHolder.time.setTextColor(getColor(0, 0))
+        viewHolder.time?.setTextColor(getColor(0, 0))
         viewHolder.content.setTextColor(getColor(0, 0))
         viewHolder.itemView.setBackgroundColor(getColor(1, 0))
       }
@@ -83,7 +87,7 @@ class QuasselMessageRenderer @Inject constructor(
       }
     }
     val textSize = appearanceSettings.textSize.toFloat()
-    viewHolder.time.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize)
+    viewHolder.time?.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize)
     viewHolder.content.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize)
   }
 
@@ -322,7 +326,7 @@ class QuasselMessageRenderer @Inject constructor(
       }
       Message_Type.Server,
       Message_Type.Info,
-      Message_Type.Error        -> FormattedMessage(
+      Message_Type.Error -> FormattedMessage(
         message.content.messageId,
         timeFormatter.format(message.content.time.atZone(zoneId)),
         formatContent(context, message.content.content, highlight),
@@ -330,7 +334,7 @@ class QuasselMessageRenderer @Inject constructor(
         isExpanded = message.isExpanded,
         isSelected = message.isSelected
       )
-      Message_Type.Topic        -> FormattedMessage(
+      Message_Type.Topic -> FormattedMessage(
         message.content.messageId,
         timeFormatter.format(message.content.time.atZone(zoneId)),
         formatContent(context, message.content.content, highlight),
@@ -338,7 +342,15 @@ class QuasselMessageRenderer @Inject constructor(
         isExpanded = message.isExpanded,
         isSelected = message.isSelected
       )
-      else                      -> FormattedMessage(
+      DayChange          -> FormattedMessage(
+        message.content.messageId,
+        "",
+        dateFormatter.format(message.content.time.atZone(zoneId)),
+        isMarkerLine = false,
+        isExpanded = false,
+        isSelected = false
+      )
+      else               -> FormattedMessage(
         message.content.messageId,
         timeFormatter.format(message.content.time.atZone(zoneId)),
         SpanFormatter.format(
diff --git a/app/src/main/res/layout/widget_chatmessage_daychange.xml b/app/src/main/res/layout/widget_chatmessage_daychange.xml
new file mode 100644
index 000000000..9b5338654
--- /dev/null
+++ b/app/src/main/res/layout/widget_chatmessage_daychange.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:background="?attr/backgroundMenuItem"
+  android:orientation="vertical"
+  android:textAppearance="?android:attr/textAppearanceListItemSmall">
+
+  <View
+    android:layout_width="match_parent"
+    android:layout_height="1dp"
+    android:background="?colorDivider" />
+
+  <LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:paddingBottom="@dimen/message_vertical"
+    android:paddingEnd="@dimen/message_horizontal"
+    android:paddingLeft="@dimen/message_horizontal"
+    android:paddingRight="@dimen/message_horizontal"
+    android:paddingStart="@dimen/message_horizontal"
+    android:paddingTop="@dimen/message_vertical">
+
+    <TextView
+      android:id="@+id/content"
+      android:layout_width="0dip"
+      android:layout_height="wrap_content"
+      android:layout_weight="1"
+      android:gravity="center"
+      android:textColor="?attr/colorForeground"
+      android:textStyle="bold"
+      tools:text="27.03.2018" />
+  </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt
index 9f3645142..274e2465f 100644
--- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt
+++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt
@@ -2,7 +2,9 @@ package de.kuschku.quasseldroid.persistence
 
 import android.arch.lifecycle.LiveData
 import android.arch.paging.DataSource
+import android.arch.persistence.db.SimpleSQLiteQuery
 import android.arch.persistence.db.SupportSQLiteDatabase
+import android.arch.persistence.db.SupportSQLiteQuery
 import android.arch.persistence.room.*
 import android.arch.persistence.room.migration.Migration
 import android.content.Context
@@ -60,6 +62,15 @@ abstract class QuasselDatabase : RoomDatabase() {
     )
     fun findByBufferIdPaged(bufferId: Int, type: Int): DataSource.Factory<Int, DatabaseMessage>
 
+    @RawQuery(observedEntities = [DatabaseMessage::class])
+    fun findMessagesRawPaged(query: SupportSQLiteQuery): DataSource.Factory<Int, DatabaseMessage>
+
+    @RawQuery
+    fun findDaysRaw(query: SupportSQLiteQuery): List<Instant>
+
+    @RawQuery
+    fun findMessagesRaw(query: SupportSQLiteQuery): List<DatabaseMessage>
+
     @Query("SELECT * FROM message WHERE bufferId = :bufferId ORDER BY messageId DESC LIMIT 1")
     fun findLastByBufferId(bufferId: Int): DatabaseMessage?
 
@@ -163,4 +174,72 @@ fun QuasselDatabase.MessageDao.clearMessages(
   idRange: kotlin.ranges.IntRange
 ) {
   this.clearMessages(bufferId, idRange.first, idRange.last)
-}
\ No newline at end of file
+}
+
+fun QuasselDatabase.MessageDao.findByBufferIdPagedWithDayChange(bufferId: Int, type: Int) =
+  this.findMessagesRawPaged(SimpleSQLiteQuery("""
+SELECT t.*
+FROM
+  (
+    SELECT
+      messageId,
+      time,
+      type,
+      flag,
+      bufferId,
+      sender,
+      senderPrefixes,
+      content
+    FROM message
+    WHERE bufferId = ?
+          AND type & ~? > 0
+    UNION ALL
+    SELECT DISTINCT
+      strftime('%s', date(datetime(time / 1000, 'unixepoch')), 'utc') * -1000 AS messageId,
+      strftime('%s', date(datetime(time / 1000, 'unixepoch')), 'utc') * 1000  AS time,
+      8192                                                             AS type,
+      0                                                                AS flag,
+      ?                                                                AS bufferId,
+      ''                                                               AS sender,
+      ''                                                               AS senderPrefixes,
+      ''                                                               AS content
+    FROM message
+    WHERE bufferId = ?
+          AND type & ~? > 0
+  ) t
+ORDER BY time DESC
+  """, arrayOf(bufferId, type, bufferId, bufferId, type)))
+
+fun QuasselDatabase.MessageDao.findByBufferIdWithDayChange(bufferId: Int, type: Int) =
+  this.findMessagesRaw(SimpleSQLiteQuery("""
+SELECT t.*
+FROM
+  (
+    SELECT
+      messageId,
+      time,
+      type,
+      flag,
+      bufferId,
+      sender,
+      senderPrefixes,
+      content
+    FROM message
+    WHERE bufferId = ?
+          AND type & ~? > 0
+    UNION ALL
+    SELECT DISTINCT
+      strftime('%s', date(datetime(time / 1000, 'unixepoch'))) * -1000 AS messageId,
+      strftime('%s', date(datetime(time / 1000, 'unixepoch'))) * 1000  AS time,
+      8192                                                             AS type,
+      0                                                                AS flag,
+      ?                                                                AS bufferId,
+      ''                                                               AS sender,
+      ''                                                               AS senderPrefixes,
+      ''                                                               AS content
+    FROM message
+    WHERE bufferId = ?
+          AND type & ~? > 0
+  ) t
+ORDER BY time DESC
+  """, arrayOf(bufferId, type, bufferId, bufferId, type)))
\ No newline at end of file
-- 
GitLab