From cf7c05f79eed05b85677cf9ec6bee6c8b0efc0fd Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Wed, 28 Feb 2018 16:33:35 +0100
Subject: [PATCH] Added enter key option, improved hostmask option

---
 .../quasseldroid_ng/ui/chat/ChatActivity.kt   |  29 +++-
 .../chat/messages/QuasselMessageRenderer.kt   |  22 ++-
 .../ui/settings/data/AppearanceSettings.kt    |  32 ++++-
 .../ui/settings/data/Settings.kt              |  24 ++--
 .../util/helper/SpannedHelper.kt              | 129 ++++++++++++++++++
 .../util/irc/format/IrcFormatDeserializer.kt  |   4 +
 .../util/quassel/IrcUserUtils.kt              |   6 +-
 app/src/main/res/layout/layout_editor.xml     |   2 +-
 .../main/res/values/strings_preferences.xml   |  13 ++
 app/src/main/res/xml/preferences.xml          |   7 +
 10 files changed, 239 insertions(+), 29 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/SpannedHelper.kt

diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt
index beb5782ac..df74b7053 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt
@@ -15,7 +15,9 @@ import android.support.v4.widget.DrawerLayout
 import android.support.v7.app.ActionBarDrawerToggle
 import android.support.v7.widget.Toolbar
 import android.text.InputType
+import android.text.Spanned
 import android.view.*
+import android.view.inputmethod.EditorInfo
 import android.widget.EditText
 import android.widget.ImageButton
 import butterknife.BindView
@@ -32,14 +34,12 @@ import de.kuschku.quasseldroid_ng.Keys
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.persistence.QuasselDatabase
 import de.kuschku.quasseldroid_ng.ui.settings.SettingsActivity
+import de.kuschku.quasseldroid_ng.ui.settings.data.AppearanceSettings
 import de.kuschku.quasseldroid_ng.ui.settings.data.BacklogSettings
 import de.kuschku.quasseldroid_ng.ui.settings.data.Settings
 import de.kuschku.quasseldroid_ng.ui.viewmodel.QuasselViewModel
 import de.kuschku.quasseldroid_ng.util.AndroidHandlerThread
-import de.kuschku.quasseldroid_ng.util.helper.editApply
-import de.kuschku.quasseldroid_ng.util.helper.invoke
-import de.kuschku.quasseldroid_ng.util.helper.let
-import de.kuschku.quasseldroid_ng.util.helper.sharedPreferences
+import de.kuschku.quasseldroid_ng.util.helper.*
 import de.kuschku.quasseldroid_ng.util.irc.format.IrcFormatSerializer
 import de.kuschku.quasseldroid_ng.util.service.ServiceBoundActivity
 import de.kuschku.quasseldroid_ng.util.ui.MaterialContentLoadingProgressBar
@@ -117,6 +117,17 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
       send()
     }
 
+    chatline.imeOptions = when (appearanceSettings.inputEnter) {
+      AppearanceSettings.InputEnterMode.EMOJI -> listOf(
+        EditorInfo.IME_ACTION_NONE,
+        EditorInfo.IME_FLAG_NO_EXTRACT_UI
+      )
+      AppearanceSettings.InputEnterMode.SEND  -> listOf(
+        EditorInfo.IME_ACTION_SEND,
+        EditorInfo.IME_FLAG_NO_EXTRACT_UI
+      )
+    }.fold(0, Int::or)
+
     chatline.setOnKeyListener { _, keyCode, event ->
       if (event.hasNoModifiers() && (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)) {
         send()
@@ -171,9 +182,13 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
         viewModel.getBuffer().let { bufferId ->
           session.bufferSyncer?.bufferInfo(bufferId)?.also { bufferInfo ->
             val output = mutableListOf<IAliasManager.Command>()
-            session.aliasManager?.processInput(
-              bufferInfo, ircFormatSerializer.toEscapeCodes(text), output
-            )
+            for (line in text.lineSequence()) {
+              session.aliasManager?.processInput(
+                bufferInfo,
+                if (line is Spanned) ircFormatSerializer.toEscapeCodes(line) else line.toString(),
+                output
+              )
+            }
             for (command in output) {
               session.rpcHandler?.sendInput(command.buffer, command.message)
             }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/messages/QuasselMessageRenderer.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/messages/QuasselMessageRenderer.kt
index 993162c30..8d141bc8e 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/messages/QuasselMessageRenderer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/messages/QuasselMessageRenderer.kt
@@ -324,28 +324,38 @@ class QuasselMessageRenderer(
     }
   }
 
-  private fun formatNickImpl(sender: String, colorize: Boolean, hostmask: Boolean): CharSequence {
-    val nick = IrcUserUtils.nick(sender)
-    val content = if (hostmask) sender else nick
-    val spannableString = SpannableString(content)
+  private fun formatNickNickImpl(nick: String, colorize: Boolean): CharSequence {
+    val spannableString = SpannableString(nick)
     if (colorize) {
       val senderColor = IrcUserUtils.senderColor(nick)
       spannableString.setSpan(
         ForegroundColorSpan(senderColors[senderColor % senderColors.size]),
         0,
-        content.length,
+        nick.length,
         SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
       )
     }
     spannableString.setSpan(
       StyleSpan(Typeface.BOLD),
       0,
-      content.length,
+      nick.length,
       SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
     )
     return spannableString
   }
 
+  private fun formatNickImpl(sender: String, colorize: Boolean, hostmask: Boolean): CharSequence {
+    val nick = IrcUserUtils.nick(sender)
+    val mask = IrcUserUtils.mask(sender)
+    val formattedNick = formatNickNickImpl(nick, colorize)
+
+    return if (hostmask) {
+      SpanFormatter.format("%s (%s)", formattedNick, mask)
+    } else {
+      formattedNick
+    }
+  }
+
   private fun formatNick(sender: String, self: Boolean,
                          highlight: Boolean, showHostmask: Boolean) =
     when (appearanceSettings.colorizeNicknames) {
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/AppearanceSettings.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/AppearanceSettings.kt
index f3f53c065..0419da8de 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/AppearanceSettings.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/AppearanceSettings.kt
@@ -6,6 +6,7 @@ import de.kuschku.quasseldroid_ng.R
 data class AppearanceSettings(
   val showPrefix: ShowPrefixMode = ShowPrefixMode.HIGHEST,
   val colorizeNicknames: ColorizeNicknamesMode = ColorizeNicknamesMode.ALL_BUT_MINE,
+  val inputEnter: InputEnterMode = InputEnterMode.EMOJI,
   val colorizeMirc: Boolean = true,
   val useMonospace: Boolean = false,
   val showSeconds: Boolean = false,
@@ -17,13 +18,33 @@ data class AppearanceSettings(
   enum class ColorizeNicknamesMode {
     ALL,
     ALL_BUT_MINE,
-    NONE
+    NONE;
+
+    companion object {
+      private val map = values().associateBy { it.name }
+      fun of(name: String) = map[name]
+    }
+  }
+
+  enum class InputEnterMode {
+    EMOJI,
+    SEND;
+
+    companion object {
+      private val map = values().associateBy { it.name }
+      fun of(name: String) = map[name]
+    }
   }
 
   enum class ShowPrefixMode {
     ALL,
     HIGHEST,
-    NONE
+    NONE;
+
+    companion object {
+      private val map = values().associateBy { it.name }
+      fun of(name: String) = map[name]
+    }
   }
 
   enum class Theme(@StyleRes val style: Int) {
@@ -33,7 +54,12 @@ data class AppearanceSettings(
     SOLARIZED_LIGHT(R.style.Theme_ChatTheme_Solarized_Light),
     SOLARIZED_DARK(R.style.Theme_ChatTheme_Solarized_Dark),
     GRUVBOX_LIGHT(R.style.Theme_ChatTheme_Gruvbox_Light),
-    GRUVBOX_DARK(R.style.Theme_ChatTheme_Gruvbox_Dark)
+    GRUVBOX_DARK(R.style.Theme_ChatTheme_Gruvbox_Dark);
+
+    companion object {
+      private val map = values().associateBy { it.name }
+      fun of(name: String) = map[name]
+    }
   }
 
   companion object {
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/Settings.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/Settings.kt
index 3c820d012..2e2e88574 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/Settings.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/settings/data/Settings.kt
@@ -8,12 +8,12 @@ import de.kuschku.quasseldroid_ng.util.helper.sharedPreferences
 object Settings {
   fun appearance(context: Context) = context.sharedPreferences {
     AppearanceSettings(
-      theme = Theme.valueOf(
+      theme = Theme.of(
         getString(
           context.getString(R.string.preference_theme_key),
-          AppearanceSettings.DEFAULT.theme.name
+          null
         )
-      ),
+      ) ?: AppearanceSettings.DEFAULT.theme,
       useMonospace = getBoolean(
         context.getString(R.string.preference_monospace_key),
         AppearanceSettings.DEFAULT.useMonospace
@@ -26,18 +26,24 @@ object Settings {
         context.getString(R.string.preference_use_24h_clock_key),
         AppearanceSettings.DEFAULT.use24hClock
       ),
-      showPrefix = ShowPrefixMode.valueOf(
+      showPrefix = ShowPrefixMode.of(
         getString(
           context.getString(R.string.preference_show_prefix_key),
-          AppearanceSettings.DEFAULT.showPrefix.name
+          null
         )
-      ),
-      colorizeNicknames = ColorizeNicknamesMode.valueOf(
+      ) ?: AppearanceSettings.DEFAULT.showPrefix,
+      colorizeNicknames = ColorizeNicknamesMode.of(
         getString(
           context.getString(R.string.preference_colorize_nicknames_key),
-          AppearanceSettings.DEFAULT.colorizeNicknames.name
+          null
         )
-      ),
+      ) ?: AppearanceSettings.DEFAULT.colorizeNicknames,
+      inputEnter = InputEnterMode.of(
+        getString(
+          context.getString(R.string.preference_input_enter_key),
+          null
+        )
+      ) ?: AppearanceSettings.DEFAULT.inputEnter,
       colorizeMirc = getBoolean(
         context.getString(R.string.preference_colorize_mirc_key),
         AppearanceSettings.DEFAULT.colorizeMirc
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/SpannedHelper.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/SpannedHelper.kt
new file mode 100644
index 000000000..496344b09
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/SpannedHelper.kt
@@ -0,0 +1,129 @@
+package de.kuschku.quasseldroid_ng.util.helper
+
+private class DelimitedRangesSequence(
+  private val input: CharSequence,
+  private val startIndex: Int,
+  private val limit: Int,
+  private val getNextMatch: CharSequence.(Int) -> Pair<Int, Int>?
+) : Sequence<IntRange> {
+  override fun iterator(): Iterator<IntRange> = object : Iterator<IntRange> {
+    var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
+    var currentStartIndex: Int = startIndex.coerceIn(0, input.length)
+    var nextSearchIndex: Int = currentStartIndex
+    var nextItem: IntRange? = null
+    var counter: Int = 0
+
+    private fun calcNext() {
+      if (nextSearchIndex < 0) {
+        nextState = 0
+        nextItem = null
+      } else {
+        if (limit > 0 && ++counter >= limit || nextSearchIndex > input.length) {
+          nextItem = currentStartIndex..input.lastIndex
+          nextSearchIndex = -1
+        } else {
+          val match = input.getNextMatch(nextSearchIndex)
+          if (match == null) {
+            nextItem = currentStartIndex..input.lastIndex
+            nextSearchIndex = -1
+          } else {
+            val (index, length) = match
+            nextItem = currentStartIndex..index - 1
+            currentStartIndex = index + length
+            nextSearchIndex = currentStartIndex + if (length == 0) 1 else 0
+          }
+        }
+        nextState = 1
+      }
+    }
+
+    override fun next(): IntRange {
+      if (nextState == -1)
+        calcNext()
+      if (nextState == 0)
+        throw NoSuchElementException()
+      val result = nextItem as IntRange
+      // Clean next to avoid keeping reference on yielded instance
+      nextItem = null
+      nextState = -1
+      return result
+    }
+
+    override fun hasNext(): Boolean {
+      if (nextState == -1)
+        calcNext()
+      return nextState == 1
+    }
+  }
+}
+
+internal fun CharSequence.regionMatchesImpl(thisOffset: Int, other: CharSequence, otherOffset: Int,
+                                            length: Int, ignoreCase: Boolean): Boolean {
+  if ((otherOffset < 0) || (thisOffset < 0) || (thisOffset > this.length - length)
+      || (otherOffset > other.length - length)) {
+    return false
+  }
+
+  for (index in 0 until length) {
+    if (!this[thisOffset + index].equals(other[otherOffset + index], ignoreCase))
+      return false
+  }
+  return true
+}
+
+private fun CharSequence.findAnyOf(strings: Collection<String>, startIndex: Int,
+                                   ignoreCase: Boolean, last: Boolean): Pair<Int, String>? {
+  if (!ignoreCase && strings.size == 1) {
+    val string = strings.single()
+    val index = if (!last) indexOf(string, startIndex) else lastIndexOf(string, startIndex)
+    return if (index < 0) null else index to string
+  }
+
+  val indices = if (!last) startIndex.coerceAtLeast(0)..length else startIndex.coerceAtMost(
+    lastIndex
+  ) downTo 0
+
+  if (this is String) {
+    for (index in indices) {
+      val matchingString = strings.firstOrNull {
+        it.regionMatches(
+          0, this, index, it.length, ignoreCase
+        )
+      }
+      if (matchingString != null)
+        return index to matchingString
+    }
+  } else {
+    for (index in indices) {
+      val matchingString = strings.firstOrNull {
+        it.regionMatchesImpl(
+          0, this, index, it.length, ignoreCase
+        )
+      }
+      if (matchingString != null)
+        return index to matchingString
+    }
+  }
+
+  return null
+}
+
+private fun CharSequence.rangesDelimitedBy(delimiters: Array<out String>, startIndex: Int = 0,
+                                           ignoreCase: Boolean = false,
+                                           limit: Int = 0): Sequence<IntRange> {
+  require(limit >= 0, { "Limit must be non-negative, but was $limit." })
+  val delimitersList = delimiters.asList()
+
+  return DelimitedRangesSequence(
+    this, startIndex, limit, { startIndex ->
+    findAnyOf(
+      delimitersList, startIndex, ignoreCase = ignoreCase, last = false
+    )?.let { it.first to it.second.length }
+  })
+}
+
+fun CharSequence.splitToSequence(vararg delimiters: String, ignoreCase: Boolean = false,
+                                 limit: Int = 0): Sequence<CharSequence> =
+  rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).map { subSequence(it) }
+
+fun CharSequence.lineSequence(): Sequence<CharSequence> = splitToSequence("\r\n", "\n", "\r")
\ No newline at end of file
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt
index 2b3fe8e07..be95a8de0 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt
@@ -251,6 +251,10 @@ class IrcFormatDeserializer(private val context: Context) {
             if (colorize) color.apply(plainText, plainText.length)
             color = null
           }
+          if (hexColor != null) {
+            if (colorize) hexColor.apply(plainText, plainText.length)
+            hexColor = null
+          }
         }
         else           -> {
           // Just append it, if it’s not special
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/quassel/IrcUserUtils.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/quassel/IrcUserUtils.kt
index 738f86ef3..a259c1e11 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/quassel/IrcUserUtils.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/quassel/IrcUserUtils.kt
@@ -18,20 +18,20 @@ object IrcUserUtils {
 
   fun user(hostmask: String): String {
     return hostmask.substring(
-      hostmask.lastIndex('!', hostmask.lastIndex('@')) ?: 0,
+      (hostmask.lastIndex('!', hostmask.lastIndex('@')) ?: -1) + 1,
       hostmask.lastIndex('@') ?: hostmask.length
     )
   }
 
   fun host(hostmask: String): String {
     return hostmask.substring(
-      hostmask.lastIndex('@') ?: 0
+      (hostmask.lastIndex('@') ?: -1) + 1
     )
   }
 
   fun mask(hostmask: String): String {
     return hostmask.substring(
-      hostmask.lastIndex('!', hostmask.lastIndex('@')) ?: 0
+      (hostmask.lastIndex('!', hostmask.lastIndex('@')) ?: -1) + 1
     )
   }
 
diff --git a/app/src/main/res/layout/layout_editor.xml b/app/src/main/res/layout/layout_editor.xml
index a1e6accbb..f5ab7962b 100644
--- a/app/src/main/res/layout/layout_editor.xml
+++ b/app/src/main/res/layout/layout_editor.xml
@@ -23,7 +23,7 @@
       android:background="@android:color/transparent"
       android:gravity="center_vertical"
       android:hint="@string/label_placeholder"
-      android:imeOptions="actionSend|flagNoEnterAction|flagNoExtractUi"
+      android:imeOptions="flagNoExtractUi"
       android:inputType="textCapSentences|textAutoCorrect|textShortMessage"
       android:minHeight="?actionBarSize"
       android:paddingBottom="8dp"
diff --git a/app/src/main/res/values/strings_preferences.xml b/app/src/main/res/values/strings_preferences.xml
index 0ba6ff568..d68f77ebb 100644
--- a/app/src/main/res/values/strings_preferences.xml
+++ b/app/src/main/res/values/strings_preferences.xml
@@ -76,6 +76,19 @@
     <item>NONE</item>
   </string-array>
 
+  <string name="preference_input_enter_key" translatable="false">input_enter</string>
+  <string name="preference_input_enter_title">Enter key on keyboard</string>
+  <string name="preference_input_enter_entry_emoji">Emoji</string>
+  <string name="preference_input_enter_entry_send">Send</string>
+  <string-array name="preference_input_enter_entries">
+    <item>@string/preference_input_enter_entry_emoji</item>
+    <item>@string/preference_input_enter_entry_send</item>
+  </string-array>
+  <string-array name="preference_input_enter_entryvalues">
+    <item>EMOJI</item>
+    <item>SEND</item>
+  </string-array>
+
   <string name="preference_hostmask_key" translatable="false">hostmask</string>
   <string name="preference_hostmask_title">Show Hostmask</string>
   <string name="preference_hostmask_summary">Display the full nick!ident@host in messages</string>
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 11c9426fe..bffc6e13f 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -37,6 +37,13 @@
       android:key="@string/preference_colorize_nicknames_key"
       android:title="@string/preference_colorize_nicknames_title" />
 
+    <DropDownPreference
+      android:defaultValue="EMOJI"
+      android:entries="@array/preference_input_enter_entries"
+      android:entryValues="@array/preference_input_enter_entryvalues"
+      android:key="@string/preference_input_enter_key"
+      android:title="@string/preference_input_enter_title" />
+
     <DropDownPreference
       android:defaultValue="HIGHEST"
       android:entries="@array/preference_show_prefix_entries"
-- 
GitLab