From 096a402b7ea09df7d10baa5bedf4df96747b2833 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Sun, 18 Feb 2018 22:35:43 +0100
Subject: [PATCH] Port mIRC Color handling from -NG

---
 .../util/helper/ThemeHelper.kt                |   7 +-
 .../util/irc/format/IrcFormatDeserializer.kt  | 331 ++++++++++++++++++
 .../util/irc/format/IrcFormatSerializer.kt    | 144 ++++++++
 .../util/irc/format/spans/Copyable.kt         |   5 +
 .../format/spans/IrcBackgroundColorSpan.kt    |  11 +
 .../util/irc/format/spans/IrcBoldSpan.kt      |   8 +
 .../format/spans/IrcForegroundColorSpan.kt    |  11 +
 .../util/irc/format/spans/IrcItalicSpan.kt    |   8 +
 .../util/irc/format/spans/IrcUnderlineSpan.kt |   7 +
 9 files changed, 528 insertions(+), 4 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatSerializer.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/Copyable.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBackgroundColorSpan.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBoldSpan.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcForegroundColorSpan.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcItalicSpan.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcUnderlineSpan.kt

diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ThemeHelper.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ThemeHelper.kt
index 452086db1..137e10e6d 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ThemeHelper.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/ThemeHelper.kt
@@ -3,10 +3,9 @@ package de.kuschku.quasseldroid_ng.util.helper
 import android.content.res.Resources
 import android.content.res.TypedArray
 
-inline fun Resources.Theme.styledAttributes(vararg attributes: Int, f: TypedArray.() -> Unit) {
-  this.obtainStyledAttributes(attributes).use {
-    it.apply(f)
-  }
+inline fun <R> Resources.Theme.styledAttributes(vararg attributes: Int, f: TypedArray.() -> R)
+  = this.obtainStyledAttributes(attributes).run {
+  f()
 }
 
 inline fun <R> TypedArray.use(block: (TypedArray) -> R): R {
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
new file mode 100644
index 000000000..4a7f9048b
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt
@@ -0,0 +1,331 @@
+/*
+ * QuasselDroid - Quassel client for Android
+ * Copyright (C) 2016 Janne Koschinski
+ * Copyright (C) 2016 Ken Børge Viktil
+ * Copyright (C) 2016 Magnus Fjell
+ * Copyright (C) 2016 Martin Sandsmark <martin.sandsmark@kde.org>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid_ng.util.irc.format
+
+
+import android.content.Context
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.style.UnderlineSpan
+import de.kuschku.quasseldroid_ng.R
+import de.kuschku.quasseldroid_ng.util.helper.styledAttributes
+import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcBackgroundColorSpan
+import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcBoldSpan
+import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcForegroundColorSpan
+import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcItalicSpan
+import java.util.*
+
+/**
+ * A helper class to turn mIRC formatted Strings into Android’s SpannableStrings with the same
+ * color and format codes
+ */
+class IrcFormatDeserializer(private val context: Context) {
+
+  /**
+   * Function to handle mIRC formatted strings
+   *
+   * @param str mIRC formatted String
+   * @return a CharSequence with Android’s span format representing the input string
+   */
+  fun formatString(str: String?, colorize: Boolean): CharSequence {
+    if (str == null) return ""
+
+    val plainText = SpannableStringBuilder()
+    var bold: FormatDescription? = null
+    var italic: FormatDescription? = null
+    var underline: FormatDescription? = null
+    var color: FormatDescription? = null
+
+    // Iterating over every character
+    var normalCount = 0
+    var i = 0
+    while (i < str.length) {
+      val character = str[i]
+      when (character) {
+        CODE_BOLD      -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          // If there is an element on stack with the same code, close it
+          if (bold != null) {
+            if (colorize) bold.apply(plainText, plainText.length)
+            bold = null
+            // Otherwise create a new one
+          } else {
+            val format = fromId(character)
+            bold = FormatDescription(plainText.length, format!!)
+          }
+        }
+        CODE_ITALIC    -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          // If there is an element on stack with the same code, close it
+          if (italic != null) {
+            if (colorize) italic.apply(plainText, plainText.length)
+            italic = null
+            // Otherwise create a new one
+          } else {
+            val format = fromId(character)
+            italic = FormatDescription(plainText.length, format!!)
+          }
+        }
+        CODE_UNDERLINE -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          // If there is an element on stack with the same code, close it
+          if (underline != null) {
+            if (colorize) underline.apply(plainText, plainText.length)
+            underline = null
+            // Otherwise create a new one
+          } else {
+            val format = fromId(character)
+            underline = FormatDescription(plainText.length, format!!)
+          }
+        }
+        CODE_COLOR     -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          val foregroundStart = i + 1
+          val foregroundEnd = findEndOfNumber(str, foregroundStart)
+          // If we have a foreground element
+          if (foregroundEnd > foregroundStart) {
+            val foreground = readNumber(str, foregroundStart, foregroundEnd)
+
+            var background: Byte = -1
+            var backgroundEnd = -1
+            // If we have a background code, read it
+            if (str.length > foregroundEnd && str[foregroundEnd] == ',') {
+              backgroundEnd = findEndOfNumber(str, foregroundEnd + 1)
+              background = readNumber(str, foregroundEnd + 1, backgroundEnd)
+            }
+            // If previous element was also a color element, try to reuse background
+            if (color != null) {
+              // Apply old format
+              if (colorize) color.apply(plainText, plainText.length)
+              // Reuse old background, if possible
+              if (background.toInt() == -1)
+                background = (color.format as ColorIrcFormat).background
+            }
+            // Add new format
+            color = FormatDescription(plainText.length, ColorIrcFormat(foreground, background))
+
+            // i points in front of the next character
+            i = (if (backgroundEnd == -1) foregroundEnd else backgroundEnd) - 1
+
+            // Otherwise assume this is a closing tag
+          } else if (color != null) {
+            if (colorize) color.apply(plainText, plainText.length)
+            color = null
+          }
+        }
+        CODE_SWAP      -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          // If we have a color tag before, apply it, and create a new one with swapped colors
+          if (color != null) {
+            if (colorize) color.apply(plainText, plainText.length)
+            color = FormatDescription(
+              plainText.length, (color.format as ColorIrcFormat).copySwapped()
+            )
+          }
+        }
+        CODE_RESET     -> {
+          plainText.append(str.substring(i - normalCount, i))
+          normalCount = 0
+
+          // End all formatting tags
+          if (bold != null) {
+            if (colorize) bold.apply(plainText, plainText.length)
+            bold = null
+          }
+          if (italic != null) {
+            if (colorize) italic.apply(plainText, plainText.length)
+            italic = null
+          }
+          if (underline != null) {
+            if (colorize) underline.apply(plainText, plainText.length)
+            underline = null
+          }
+          if (color != null) {
+            if (colorize) color.apply(plainText, plainText.length)
+            color = null
+          }
+        }
+        else           -> {
+          // Just append it, if it’s not special
+          normalCount++
+        }
+      }
+      i++
+    }
+
+    // End all formatting tags
+    if (bold != null) {
+      if (colorize) bold.apply(plainText, plainText.length)
+    }
+    if (italic != null) {
+      if (colorize) italic.apply(plainText, plainText.length)
+    }
+    if (underline != null) {
+      if (colorize) underline.apply(plainText, plainText.length)
+    }
+    if (color != null) {
+      if (colorize) color.apply(plainText, plainText.length)
+    }
+    plainText.append(str.substring(str.length - normalCount, str.length))
+    return plainText
+  }
+
+  private interface IrcFormat {
+    fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int)
+
+    fun id(): Byte
+  }
+
+  private class FormatDescription(val start: Int, val format: IrcFormat) {
+
+    fun apply(editable: SpannableStringBuilder, end: Int) {
+      format.applyTo(editable, start, end)
+    }
+  }
+
+  private class ItalicIrcFormat : IrcFormat {
+    override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
+      editable.setSpan(IrcItalicSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+    }
+
+    override fun id(): Byte {
+      return CODE_ITALIC.toByte()
+    }
+  }
+
+  private class UnderlineIrcFormat : IrcFormat {
+    override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
+      editable.setSpan(UnderlineSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+    }
+
+    override fun id(): Byte {
+      return CODE_UNDERLINE.toByte()
+    }
+  }
+
+  private class BoldIrcFormat : IrcFormat {
+    override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
+      editable.setSpan(IrcBoldSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+    }
+
+    override fun id(): Byte {
+      return CODE_BOLD.toByte()
+    }
+  }
+
+  private inner class ColorIrcFormat(val foreground: Byte, val background: Byte) : IrcFormat {
+
+    override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
+      val mircColors = context.theme.styledAttributes(
+        R.attr.mircColor0, R.attr.mircColor1, R.attr.mircColor2, R.attr.mircColor3,
+        R.attr.mircColor4, R.attr.mircColor5, R.attr.mircColor6, R.attr.mircColor7,
+        R.attr.mircColor8, R.attr.mircColor9, R.attr.mircColorA, R.attr.mircColorB,
+        R.attr.mircColorC, R.attr.mircColorD, R.attr.mircColorE, R.attr.mircColorF
+      ) {
+        IntArray(16) {
+          getColor(it, 0)
+        }
+      }
+
+      if (foreground.toInt() != -1 && foreground.toInt() != 99) {
+        editable.setSpan(
+          IrcForegroundColorSpan(foreground.toInt(), mircColors[foreground % 16]), from, to,
+          Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+        )
+      }
+      if (background.toInt() != -1 && background.toInt() != 99) {
+        editable.setSpan(
+          IrcBackgroundColorSpan(background.toInt(), mircColors[background % 16]), from, to,
+          Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+        )
+      }
+    }
+
+    fun copySwapped(): ColorIrcFormat {
+      return ColorIrcFormat(background, foreground)
+    }
+
+    override fun id(): Byte {
+      return CODE_COLOR.toByte()
+    }
+  }
+
+  companion object {
+    val CODE_BOLD = 0x02.toChar()
+    val CODE_COLOR = 0x03.toChar()
+    val CODE_ITALIC = 0x1D.toChar()
+    val CODE_UNDERLINE = 0x1F.toChar()
+    val CODE_SWAP = 0x16.toChar()
+    val CODE_RESET = 0x0F.toChar()
+
+    /**
+     * Try to read a number from a String in specified bounds
+     *
+     * @param str   String to be read from
+     * @param start Start index (inclusive)
+     * @param end   End index (exclusive)
+     * @return The byte represented by the digits read from the string
+     */
+    fun readNumber(str: String, start: Int, end: Int): Byte {
+      val result = str.substring(start, end)
+      return if (result.isEmpty())
+        -1
+      else
+        Integer.parseInt(result, 10).toByte()
+    }
+
+    /**
+     * @param str   String to be searched in
+     * @param start Start position (inclusive)
+     * @return Index of first character that is not a digit
+     */
+    private fun findEndOfNumber(str: String, start: Int): Int {
+      val validCharCodes = HashSet(Arrays.asList('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'))
+      val searchFrame = str.substring(start)
+      var i = 0
+      while (i < 2 && i < searchFrame.length) {
+        if (!validCharCodes.contains(searchFrame[i])) {
+          break
+        }
+        i++
+      }
+      return start + i
+    }
+
+    private fun fromId(id: Char) = when (id) {
+      CODE_BOLD      -> BoldIrcFormat()
+      CODE_ITALIC    -> ItalicIrcFormat()
+      CODE_UNDERLINE -> UnderlineIrcFormat()
+      else           -> null
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatSerializer.kt
new file mode 100644
index 000000000..5914434b0
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatSerializer.kt
@@ -0,0 +1,144 @@
+package de.kuschku.quasseldroid_ng.util.irc.format
+
+import android.content.Context
+import android.text.Spanned
+import android.text.style.BackgroundColorSpan
+import android.text.style.CharacterStyle
+import android.text.style.ForegroundColorSpan
+import android.text.style.UnderlineSpan
+import de.kuschku.quasseldroid_ng.R
+import de.kuschku.quasseldroid_ng.util.helper.styledAttributes
+import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcBackgroundColorSpan
+import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcBoldSpan
+import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcForegroundColorSpan
+import de.kuschku.quasseldroid_ng.util.irc.format.spans.IrcItalicSpan
+import java.util.*
+
+class IrcFormatSerializer internal constructor(private val context: Context) {
+
+  fun toEscapeCodes(text: Spanned): String {
+    val out = StringBuilder()
+    withinParagraph(out, text, 0, text.length)
+    return out.toString()
+  }
+
+  private fun withinParagraph(out: StringBuilder, text: Spanned,
+                              start: Int, end: Int) {
+    var next: Int
+    var foreground = -1
+    var background = -1
+    var bold = false
+    var underline = false
+    var italic = false
+
+    var i = start
+    while (i < end) {
+      next = text.nextSpanTransition(i, end, CharacterStyle::class.java)
+      val style = text.getSpans(i, next, CharacterStyle::class.java)
+
+      var afterForeground = -1
+      var afterBackground = -1
+      var afterBold = false
+      var afterUnderline = false
+      var afterItalic = false
+
+      for (aStyle in style) {
+        if (text.getSpanFlags(aStyle) and Spanned.SPAN_COMPOSING != 0)
+          continue
+
+        if (aStyle is IrcBoldSpan) {
+          afterBold = true
+        } else if (aStyle is IrcItalicSpan) {
+          afterItalic = true
+        } else if (aStyle is UnderlineSpan) {
+          afterUnderline = true
+        } else if (aStyle is IrcForegroundColorSpan) {
+          afterForeground = aStyle.mircColor
+        } else if (aStyle is IrcBackgroundColorSpan) {
+          afterBackground = aStyle.mircColor
+        } else if (aStyle is ForegroundColorSpan) {
+          afterForeground = 0
+        } else if (aStyle is BackgroundColorSpan) {
+          afterBackground = 0
+        }
+      }
+
+      if (afterBold != bold) {
+        out.append(CODE_BOLD)
+      }
+
+      if (afterUnderline != underline) {
+        out.append(CODE_UNDERLINE)
+      }
+
+      if (afterItalic != italic) {
+        out.append(CODE_ITALIC)
+      }
+
+      if (afterForeground != foreground || afterBackground != background) {
+        if (afterForeground == background && afterBackground == foreground) {
+          out.append(CODE_SWAP)
+        } else {
+          out.append(CODE_COLOR)
+          if (afterBackground == -1) {
+            if (afterForeground == -1) {
+              // Foreground changed from a value to null, we don’t set any new foreground
+              // Background changed from a value to null, we don’t set any new background
+            } else {
+              out.append(CODE_COLOR)
+              out.append(String.format(Locale.US, "%02d", afterForeground))
+            }
+          } else if (background == afterBackground) {
+            if (afterForeground == -1) {
+              out.append(
+                String.format(
+                  Locale.US, "%02d",
+                  context.theme.styledAttributes(R.attr.colorForegroundMirc) {
+                    getColor(0, 0)
+                  }
+                )
+              )
+            } else {
+              out.append(String.format(Locale.US, "%02d", afterForeground))
+            }
+          } else {
+            if (afterForeground == -1) {
+              out.append(
+                String.format(
+                  Locale.US, "%02d,%02d",
+                  context.theme.styledAttributes(R.attr.colorForegroundMirc) {
+                    getColor(0, 0)
+                  },
+                  afterBackground
+                )
+              )
+            } else {
+              out.append(String.format(Locale.US, "%02d,%02d", afterForeground, afterBackground))
+            }
+          }
+        }
+      }
+
+      out.append(text.subSequence(i, next))
+
+      bold = afterBold
+      italic = afterItalic
+      underline = afterUnderline
+      background = afterBackground
+      foreground = afterForeground
+      i = next
+    }
+
+    if (bold || italic || underline || background != -1 || foreground != -1)
+      out.append(CODE_RESET)
+  }
+
+  companion object {
+    val CODE_BOLD: Char = 0x02.toChar()
+    val CODE_COLOR: Char = 0x03.toChar()
+    val CODE_ITALIC: Char = 0x1D.toChar()
+    val CODE_UNDERLINE: Char = 0x1F.toChar()
+    val CODE_SWAP: Char = 0x16.toChar()
+    val CODE_RESET: Char = 0x0F.toChar()
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/Copyable.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/Copyable.kt
new file mode 100644
index 000000000..dd103d016
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/Copyable.kt
@@ -0,0 +1,5 @@
+package de.kuschku.quasseldroid_ng.util.irc.format.spans
+
+interface Copyable<out T> {
+  fun copy(): T
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBackgroundColorSpan.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBackgroundColorSpan.kt
new file mode 100644
index 000000000..5927da7b0
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBackgroundColorSpan.kt
@@ -0,0 +1,11 @@
+package de.kuschku.quasseldroid_ng.util.irc.format.spans
+
+import android.support.annotation.ColorInt
+import android.text.style.BackgroundColorSpan
+
+class IrcBackgroundColorSpan(
+  val mircColor: Int,
+  @ColorInt color: Int
+) : BackgroundColorSpan(color), Copyable<IrcBackgroundColorSpan> {
+  override fun copy() = IrcBackgroundColorSpan(mircColor, backgroundColor)
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBoldSpan.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBoldSpan.kt
new file mode 100644
index 000000000..ddaeec36b
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcBoldSpan.kt
@@ -0,0 +1,8 @@
+package de.kuschku.quasseldroid_ng.util.irc.format.spans
+
+import android.graphics.Typeface
+import android.text.style.StyleSpan
+
+class IrcBoldSpan : StyleSpan(Typeface.BOLD), Copyable<IrcBoldSpan> {
+  override fun copy() = IrcBoldSpan()
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcForegroundColorSpan.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcForegroundColorSpan.kt
new file mode 100644
index 000000000..70461675a
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcForegroundColorSpan.kt
@@ -0,0 +1,11 @@
+package de.kuschku.quasseldroid_ng.util.irc.format.spans
+
+import android.support.annotation.ColorInt
+import android.text.style.ForegroundColorSpan
+
+class IrcForegroundColorSpan(
+  val mircColor: Int,
+  @ColorInt color: Int
+) : ForegroundColorSpan(color), Copyable<IrcForegroundColorSpan> {
+  override fun copy() = IrcForegroundColorSpan(mircColor, foregroundColor)
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcItalicSpan.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcItalicSpan.kt
new file mode 100644
index 000000000..d38f195e5
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcItalicSpan.kt
@@ -0,0 +1,8 @@
+package de.kuschku.quasseldroid_ng.util.irc.format.spans
+
+import android.graphics.Typeface
+import android.text.style.StyleSpan
+
+class IrcItalicSpan : StyleSpan(Typeface.ITALIC), Copyable<IrcItalicSpan> {
+  override fun copy() = IrcItalicSpan()
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcUnderlineSpan.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcUnderlineSpan.kt
new file mode 100644
index 000000000..447a11bd7
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/spans/IrcUnderlineSpan.kt
@@ -0,0 +1,7 @@
+package de.kuschku.quasseldroid_ng.util.irc.format.spans
+
+import android.text.style.UnderlineSpan
+
+class IrcUnderlineSpan : UnderlineSpan(), Copyable<IrcUnderlineSpan> {
+  override fun copy() = IrcUnderlineSpan()
+}
-- 
GitLab