From 166c71933b996e8f5cd0d0bf918c4c6002c9808c Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Sun, 10 Jun 2018 12:51:52 +0200
Subject: [PATCH] Implement channel links

---
 .../service/QuasselNotificationBackend.kt     | 15 +---
 .../quasseldroid/ui/chat/ChatActivity.kt      | 68 +++++++++++++++++--
 .../chat/info/channel/ChannelInfoFragment.kt  |  3 +-
 .../ui/chat/info/user/UserInfoFragment.kt     |  3 +-
 .../chat/messages/QuasselMessageRenderer.kt   | 45 ++++++++----
 .../util/irc/format/ContentFormatter.kt       | 45 ++++++++++--
 .../persistence/QuasselBacklogStorage.kt      |  1 +
 .../persistence/QuasselDatabase.kt            | 10 ++-
 8 files changed, 150 insertions(+), 40 deletions(-)

diff --git a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt
index 7c1a93138..eefcec978 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt
@@ -49,6 +49,7 @@ import de.kuschku.quasseldroid.util.helper.loadWithFallbacks
 import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.ui.TextDrawable
+import de.kuschku.quasseldroid.viewmodel.EditorViewModel
 import javax.inject.Inject
 
 class QuasselNotificationBackend @Inject constructor(
@@ -242,21 +243,11 @@ class QuasselNotificationBackend @Inject constructor(
             selfColor = selfColor
           ))
         }
-        val content = contentFormatter.formatContent(it.content, false)
+        val content = contentFormatter.formatContent(it.content, false, it.networkId)
 
         val nickName = HostmaskHelper.nick(it.sender)
         val senderColorIndex = SenderColorUtil.senderColor(nickName)
-        val rawInitial = nickName.trimStart('-',
-                                            '_',
-                                            '[',
-                                            ']',
-                                            '{',
-                                            '}',
-                                            '|',
-                                            '`',
-                                            '^',
-                                            '.',
-                                            '\\')
+        val rawInitial = nickName.trimStart(*EditorViewModel.IGNORED_CHARS)
                            .firstOrNull() ?: nickName.firstOrNull()
         val initial = rawInitial?.toUpperCase().toString()
         val senderColor = when (messageSettings.colorizeNicknames) {
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
index c3af15170..140cc6156 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
@@ -52,6 +52,7 @@ import de.kuschku.libquassel.connection.QuasselSecurityException
 import de.kuschku.libquassel.protocol.Buffer_Type
 import de.kuschku.libquassel.protocol.Message
 import de.kuschku.libquassel.protocol.Message_Type
+import de.kuschku.libquassel.protocol.NetworkId
 import de.kuschku.libquassel.protocol.message.HandshakeMessage
 import de.kuschku.libquassel.session.Error
 import de.kuschku.libquassel.util.Optional
@@ -140,13 +141,13 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     super.onNewIntent(intent)
     if (intent != null) {
       when {
-        intent.type == "text/plain"            -> {
+        intent.type == "text/plain"                                     -> {
           chatlineFragment?.replaceText(intent.getStringExtra(Intent.EXTRA_TEXT))
           drawerLayout.closeDrawers()
         }
-        intent.hasExtra(KEY_BUFFER_ID)         -> {
+        intent.hasExtra(KEY_BUFFER_ID)                                  -> {
           viewModel.buffer.onNext(intent.getIntExtra(KEY_BUFFER_ID, -1))
-          drawerLayout.closeDrawers()
+          viewModel.bufferOpened.onNext(Unit)
           if (intent.hasExtra(KEY_ACCOUNT_ID)) {
             val accountId = intent.getLongExtra(ChatActivity.KEY_ACCOUNT_ID, -1)
             if (accountId != this.accountId) {
@@ -159,13 +160,51 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
             }
           }
         }
-        intent.hasExtra(KEY_AUTOCOMPLETE_TEXT) -> {
+        intent.hasExtra(KEY_AUTOCOMPLETE_TEXT)                          -> {
           chatlineFragment?.editorHelper?.appendText(
             intent.getStringExtra(KEY_AUTOCOMPLETE_TEXT),
             intent.getStringExtra(KEY_AUTOCOMPLETE_SUFFIX)
           )
           drawerLayout.closeDrawers()
         }
+        intent.hasExtra(KEY_NETWORK_ID) && intent.hasExtra(KEY_CHANNEL) -> {
+          val networkId = intent.getIntExtra(KEY_NETWORK_ID, -1)
+          val channel = intent.getStringExtra(KEY_CHANNEL)
+
+          viewModel.session.value?.orNull()?.also { session ->
+            val info = session.bufferSyncer?.find(
+              bufferName = channel,
+              networkId = networkId,
+              type = Buffer_Type.of(Buffer_Type.QueryBuffer)
+            )
+
+            if (info != null) {
+              ChatActivity.launch(this, bufferId = info.bufferId)
+            } else {
+
+              viewModel.allBuffers.map {
+                listOfNotNull(it.find {
+                  it.networkId == networkId && it.bufferName == channel
+                })
+              }.filter {
+                it.isNotEmpty()
+              }.firstElement().toLiveData().observe(this, Observer {
+                it?.firstOrNull()?.let { info ->
+                  ChatActivity.launch(this, bufferId = info.bufferId)
+                }
+              })
+
+              session.bufferSyncer?.find(
+                networkId = networkId,
+                type = Buffer_Type.of(Buffer_Type.StatusBuffer)
+              )?.let { statusInfo ->
+                session.rpcHandler?.sendInput(
+                  statusInfo, "/join $channel"
+                )
+              }
+            }
+          }
+        }
       }
     }
   }
@@ -891,6 +930,8 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     private const val KEY_AUTOCOMPLETE_SUFFIX = "autocomplete_suffix"
     private const val KEY_BUFFER_ID = "buffer_id"
     private const val KEY_ACCOUNT_ID = "account_id"
+    private const val KEY_NETWORK_ID = "network_id"
+    private const val KEY_CHANNEL = "channel"
 
     // Instance state keys
     private const val KEY_OPEN_BUFFER = "open_buffer"
@@ -904,10 +945,19 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
       sharedText: CharSequence? = null,
       autoCompleteText: CharSequence? = null,
       autoCompleteSuffix: String? = null,
+      channel: String? = null,
+      networkId: NetworkId? = null,
       bufferId: Int? = null,
-      accountId: Int? = null
+      accountId: Long? = null
     ) = context.startActivity(
-      intent(context, sharedText, autoCompleteText, autoCompleteSuffix, bufferId)
+      intent(context,
+             sharedText,
+             autoCompleteText,
+             autoCompleteSuffix,
+             channel,
+             networkId,
+             bufferId,
+             accountId)
     )
 
     fun intent(
@@ -915,6 +965,8 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
       sharedText: CharSequence? = null,
       autoCompleteText: CharSequence? = null,
       autoCompleteSuffix: String? = null,
+      channel: String? = null,
+      networkId: NetworkId? = null,
       bufferId: Int? = null,
       accountId: Long? = null
     ) = Intent(context, ChatActivity::class.java).apply {
@@ -934,6 +986,10 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
           putExtra(KEY_ACCOUNT_ID, accountId)
         }
       }
+      if (networkId != null && channel != null) {
+        putExtra(KEY_NETWORK_ID, networkId)
+        putExtra(KEY_CHANNEL, channel)
+      }
     }
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/channel/ChannelInfoFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/channel/ChannelInfoFragment.kt
index 7c82f433a..8671616b9 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/channel/ChannelInfoFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/channel/ChannelInfoFragment.kt
@@ -89,7 +89,8 @@ class ChannelInfoFragment : ServiceBoundFragment() {
     }.switchMap(IrcChannel::updates).toLiveData().observe(this, Observer { channel ->
       if (channel != null) {
         name.text = channel.name()
-        topic.text = contentFormatter.formatContent(channel.topic())
+        topic.text = contentFormatter.formatContent(channel.topic(),
+                                                    networkId = channel.network().networkId())
 
         actionEditTopic.setOnClickListener {
           TopicActivity.launch(requireContext(), buffer = arguments?.getInt("bufferId") ?: -1)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/user/UserInfoFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/user/UserInfoFragment.kt
index 0907e70fb..f169cb4c1 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/user/UserInfoFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/user/UserInfoFragment.kt
@@ -162,7 +162,8 @@ class UserInfoFragment : ServiceBoundFragment() {
         )
 
         nick.text = user.nick
-        realName.text = contentFormatter.formatContent(user.realName ?: "")
+        realName.text = contentFormatter.formatContent(user.realName ?: "",
+                                                       networkId = user.networkId)
         realName.visibleIf(!user.realName.isNullOrBlank() && user.realName != user.nick)
 
         awayMessage.text = user.awayMessage.nullIf { it.isNullOrBlank() } ?: SpannableString(
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 113af97d8..15d7c1785 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
@@ -46,6 +46,7 @@ import de.kuschku.quasseldroid.util.helper.visibleIf
 import de.kuschku.quasseldroid.util.irc.format.ContentFormatter
 import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
+import de.kuschku.quasseldroid.viewmodel.EditorViewModel
 import de.kuschku.quasseldroid.viewmodel.data.FormattedMessage
 import org.threeten.bp.ZoneId
 import org.threeten.bp.format.DateTimeFormatter
@@ -249,10 +250,12 @@ class QuasselMessageRenderer @Inject constructor(
             false
           ))
         }
-        val content = contentFormatter.formatContent(message.content.content, monochromeForeground)
+        val content = contentFormatter.formatContent(message.content.content,
+                                                     monochromeForeground,
+                                                     message.content.networkId)
         val nickName = HostmaskHelper.nick(message.content.sender)
         val senderColorIndex = SenderColorUtil.senderColor(nickName)
-        val rawInitial = nickName.trimStart('-', '_', '[', ']', '{', '}', '|', '`', '^', '.', '\\')
+        val rawInitial = nickName.trimStart(*EditorViewModel.IGNORED_CHARS)
                            .firstOrNull() ?: nickName.firstOrNull()
         val initial = rawInitial?.toUpperCase().toString()
         val useSelfColor = when (messageSettings.colorizeNicknames) {
@@ -286,7 +289,7 @@ class QuasselMessageRenderer @Inject constructor(
       Message_Type.Action       -> {
         val nickName = HostmaskHelper.nick(message.content.sender)
         val senderColorIndex = SenderColorUtil.senderColor(nickName)
-        val rawInitial = nickName.trimStart('-', '_', '[', ']', '{', '}', '|', '`', '^', '.', '\\')
+        val rawInitial = nickName.trimStart(*EditorViewModel.IGNORED_CHARS)
                            .firstOrNull() ?: nickName.firstOrNull()
         val initial = rawInitial?.toUpperCase().toString()
         val useSelfColor = when (messageSettings.colorizeNicknames) {
@@ -305,7 +308,9 @@ class QuasselMessageRenderer @Inject constructor(
             context.getString(R.string.message_format_action),
             contentFormatter.formatPrefix(message.content.senderPrefixes),
             contentFormatter.formatNick(message.content.sender, self, monochromeForeground, false),
-            contentFormatter.formatContent(message.content.content, monochromeForeground)
+            contentFormatter.formatContent(message.content.content,
+                                           monochromeForeground,
+                                           message.content.networkId)
           ),
           avatarUrls = AvatarHelper.avatar(messageSettings, message.content, avatarSize),
           fallbackDrawable = colorContext.buildTextDrawable(initial, senderColor),
@@ -323,7 +328,9 @@ class QuasselMessageRenderer @Inject constructor(
           context.getString(R.string.message_format_notice),
           contentFormatter.formatPrefix(message.content.senderPrefixes),
           contentFormatter.formatNick(message.content.sender, self, monochromeForeground, false),
-          contentFormatter.formatContent(message.content.content, monochromeForeground)
+          contentFormatter.formatContent(message.content.content,
+                                         monochromeForeground,
+                                         message.content.networkId)
         ),
         hasDayChange = message.hasDayChange,
         isMarkerLine = message.isMarkerLine,
@@ -432,7 +439,9 @@ class QuasselMessageRenderer @Inject constructor(
               monochromeForeground,
               messageSettings.showHostmaskActions
             ),
-            contentFormatter.formatContent(message.content.content, monochromeForeground)
+            contentFormatter.formatContent(message.content.content,
+                                           monochromeForeground,
+                                           message.content.networkId)
           )
         },
         hasDayChange = message.hasDayChange,
@@ -465,7 +474,9 @@ class QuasselMessageRenderer @Inject constructor(
               monochromeForeground,
               messageSettings.showHostmaskActions
             ),
-            contentFormatter.formatContent(message.content.content, monochromeForeground)
+            contentFormatter.formatContent(message.content.content,
+                                           monochromeForeground,
+                                           message.content.networkId)
           )
         },
         hasDayChange = message.hasDayChange,
@@ -502,7 +513,9 @@ class QuasselMessageRenderer @Inject constructor(
                 monochromeForeground,
                 messageSettings.showHostmaskActions
               ),
-              contentFormatter.formatContent(reason, monochromeForeground)
+              contentFormatter.formatContent(reason,
+                                             monochromeForeground,
+                                             message.content.networkId)
             )
           },
           hasDayChange = message.hasDayChange,
@@ -540,7 +553,9 @@ class QuasselMessageRenderer @Inject constructor(
                 monochromeForeground,
                 messageSettings.showHostmaskActions
               ),
-              contentFormatter.formatContent(reason, monochromeForeground)
+              contentFormatter.formatContent(reason,
+                                             monochromeForeground,
+                                             message.content.networkId)
             )
           },
           hasDayChange = message.hasDayChange,
@@ -589,7 +604,9 @@ class QuasselMessageRenderer @Inject constructor(
         id = message.content.messageId,
         time = timeFormatter.format(message.content.time.atZone(zoneId)),
         dayChange = formatDayChange(message),
-        combined = contentFormatter.formatContent(message.content.content, monochromeForeground),
+        combined = contentFormatter.formatContent(message.content.content,
+                                                  monochromeForeground,
+                                                  message.content.networkId),
         hasDayChange = message.hasDayChange,
         isMarkerLine = message.isMarkerLine,
         isExpanded = message.isExpanded,
@@ -599,7 +616,9 @@ class QuasselMessageRenderer @Inject constructor(
         id = message.content.messageId,
         time = timeFormatter.format(message.content.time.atZone(zoneId)),
         dayChange = formatDayChange(message),
-        combined = contentFormatter.formatContent(message.content.content, monochromeForeground),
+        combined = contentFormatter.formatContent(message.content.content,
+                                                  monochromeForeground,
+                                                  message.content.networkId),
         hasDayChange = message.hasDayChange,
         isMarkerLine = message.isMarkerLine,
         isExpanded = message.isExpanded,
@@ -619,7 +638,9 @@ class QuasselMessageRenderer @Inject constructor(
         id = message.content.messageId,
         time = timeFormatter.format(message.content.time.atZone(zoneId)),
         dayChange = formatDayChange(message),
-        combined = contentFormatter.formatContent(message.content.content, monochromeForeground),
+        combined = contentFormatter.formatContent(message.content.content,
+                                                  monochromeForeground,
+                                                  message.content.networkId),
         hasDayChange = message.hasDayChange,
         isMarkerLine = message.isMarkerLine,
         isExpanded = message.isExpanded,
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt
index 6cbd8fbbb..9437ecc90 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt
@@ -25,13 +25,17 @@ import android.support.annotation.ColorInt
 import android.text.SpannableString
 import android.text.Spanned
 import android.text.TextPaint
+import android.text.style.ClickableSpan
 import android.text.style.ForegroundColorSpan
 import android.text.style.StyleSpan
 import android.text.style.URLSpan
+import android.view.View
+import de.kuschku.libquassel.protocol.NetworkId
 import de.kuschku.libquassel.util.irc.HostmaskHelper
 import de.kuschku.libquassel.util.irc.SenderColorUtil
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.settings.MessageSettings
+import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
 import org.intellij.lang.annotations.Language
@@ -80,14 +84,33 @@ class ContentFormatter @Inject constructor(
   class QuasselURLSpan(text: String, private val highlight: Boolean) : URLSpan(text) {
     override fun updateDrawState(ds: TextPaint?) {
       if (ds != null) {
-        if (!highlight)
-          ds.color = ds.linkColor
+        if (!highlight) ds.color = ds.linkColor
         ds.isUnderlineText = true
       }
     }
   }
 
-  fun formatContent(content: String, highlight: Boolean = false): CharSequence {
+  class ChannelLinkSpan(private val networkId: NetworkId, private val text: String,
+                        private val highlight: Boolean) : ClickableSpan() {
+    override fun updateDrawState(ds: TextPaint?) {
+      if (ds != null) {
+        if (!highlight) ds.color = ds.linkColor
+        ds.isUnderlineText = true
+      }
+    }
+
+    override fun onClick(widget: View) {
+      ChatActivity.launch(
+        widget.context,
+        networkId = networkId,
+        channel = text
+      )
+    }
+  }
+
+  fun formatContent(content: String,
+                    highlight: Boolean = false,
+                    networkId: NetworkId?): CharSequence {
     val formattedText = ircFormatDeserializer.formatString(content, messageSettings.colorizeMirc)
     val text = SpannableString(formattedText)
 
@@ -101,10 +124,18 @@ class ContentFormatter @Inject constructor(
         )
       }
     }
-    /*
-    for (result in channelPattern.findAll(content)) {
-      text.setSpan(URLSpan(result.value), result.range.start, result.range.endInclusive, Spanned.SPAN_INCLUSIVE_INCLUSIVE)}
-    */
+
+    if (networkId != null) {
+      for (result in channelPattern.findAll(formattedText)) {
+        val group = result.groups[1]
+        if (group != null) {
+          text.setSpan(ChannelLinkSpan(networkId, group.value, highlight),
+                       group.range.start,
+                       group.range.endInclusive + 1,
+                       Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+        }
+      }
+    }
 
     return text
   }
diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt
index b79e27b9c..c429c4376 100644
--- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt
+++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt
@@ -46,6 +46,7 @@ class QuasselBacklogStorage(private val db: QuasselDatabase) : BacklogStorage {
         type = it.type,
         flag = it.flag,
         bufferId = it.bufferInfo.bufferId,
+        networkId = it.bufferInfo.networkId,
         sender = it.sender,
         senderPrefixes = it.senderPrefixes,
         realName = it.realName,
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 f2bf858a6..cc98904f8 100644
--- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt
+++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt
@@ -32,7 +32,7 @@ import io.reactivex.Flowable
 import org.threeten.bp.Instant
 
 @Database(entities = [MessageData::class, Filtered::class, SslValidityWhitelistEntry::class, SslHostnameWhitelistEntry::class, NotificationData::class],
-          version = 14)
+          version = 15)
 @TypeConverters(MessageTypeConverter::class)
 abstract class QuasselDatabase : RoomDatabase() {
   abstract fun message(): MessageDao
@@ -48,6 +48,7 @@ abstract class QuasselDatabase : RoomDatabase() {
     var type: Message_Types,
     var flag: Message_Flags,
     var bufferId: BufferId,
+    var networkId: NetworkId,
     var sender: String,
     var senderPrefixes: String,
     var realName: String,
@@ -323,6 +324,13 @@ abstract class QuasselDatabase : RoomDatabase() {
                   database.execSQL("CREATE TABLE IF NOT EXISTS `notification` (`messageId` INTEGER NOT NULL, `time` INTEGER NOT NULL, `type` INTEGER NOT NULL, `flag` INTEGER NOT NULL, `bufferId` INTEGER NOT NULL, `bufferName` TEXT NOT NULL, `bufferType` INTEGER NOT NULL, `networkId` INTEGER NOT NULL, `sender` TEXT NOT NULL, `senderPrefixes` TEXT NOT NULL, `realName` TEXT NOT NULL, `avatarUrl` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageId`));")
                   database.execSQL("CREATE  INDEX `index_notification_bufferId` ON `notification` (`bufferId`);")
                 }
+              },
+              object : Migration(14, 15) {
+                override fun migrate(database: SupportSQLiteDatabase) {
+                  database.execSQL(
+                    "ALTER TABLE message ADD networkId INT DEFAULT 0 NOT NULL;"
+                  )
+                }
               }
             ).build()
           }
-- 
GitLab