diff --git a/app/src/main/java/org/futo/circles/view/read_more/ReadMoreTextView.kt b/app/src/main/java/org/futo/circles/view/read_more/ReadMoreTextView.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ae8447ed9108e7051b26b30e92f7c41e9d860322
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/read_more/ReadMoreTextView.kt
@@ -0,0 +1,238 @@
+package org.futo.circles.view.read_more
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Rect
+import android.graphics.Typeface
+import android.text.Layout
+import android.text.TextPaint
+import android.text.TextUtils
+import android.text.style.ClickableSpan
+import android.text.style.TextAppearanceSpan
+import android.util.AttributeSet
+import android.view.View
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.content.res.use
+import androidx.core.text.buildSpannedString
+import androidx.core.text.inSpans
+import org.futo.circles.R
+import kotlin.text.Typography.ellipsis
+import kotlin.text.Typography.nbsp
+
+class ReadMoreTextView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = R.attr.readMoreTextViewStyle
+) : AppCompatTextView(context, attrs, defStyleAttr) {
+
+    private var readMoreMaxLines: Int = 2
+    private var readMoreText: String? = null
+    private var readMoreTextColor: ColorStateList? = null
+    private var bufferType: BufferType? = null
+    private var expanded: Boolean = false
+    private var originalText: CharSequence? = null
+    private var collapseText: CharSequence? = null
+
+    init {
+        context.obtainStyledAttributes(
+            attrs, R.styleable.ReadMoreTextView, defStyleAttr, 0
+        ).use { ta ->
+            readMoreMaxLines = ta.getInt(
+                R.styleable.ReadMoreTextView_readMoreMaxLines,
+                readMoreMaxLines
+            )
+            readMoreText = ta.getString(R.styleable.ReadMoreTextView_readMoreText)
+                ?.replace(' ', nbsp)
+            readMoreTextColor = ta.getColorStateList(
+                R.styleable.ReadMoreTextView_readMoreTextColor
+            ) ?: readMoreTextColor
+        }
+
+        if (hasOnClickListeners()) throw IllegalStateException("Custom onClickListener not supported")
+        super.setOnClickListener { toggle() }
+
+        if (originalText != null) invalidateText()
+
+    }
+
+    override fun setLines(lines: Int) {
+        throw IllegalStateException("Use the app:readMoreMaxLines")
+    }
+
+    override fun setMaxLines(maxLines: Int) {
+        throw IllegalStateException("Use the app:readMoreMaxLines")
+    }
+
+    override fun setEllipsize(where: TextUtils.TruncateAt?) {
+        throw IllegalStateException("Not supported")
+    }
+
+    override fun setOnClickListener(l: OnClickListener?) {
+        throw IllegalStateException("Not supported")
+    }
+
+    private fun toggle() {
+        setExpanded(!expanded)
+    }
+
+    private fun setExpanded(expanded: Boolean) {
+        if (this.expanded != expanded) {
+            this.expanded = expanded
+            invalidateText()
+        }
+    }
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+        if (w != oldw) {
+            originalText?.let { originalText ->
+                updateText(originalText, w)
+            }
+        }
+    }
+
+    override fun setText(text: CharSequence?, type: BufferType?) {
+        this.originalText = text
+        this.bufferType = type
+        updateText(text ?: "", width)
+    }
+
+    private fun updateText(text: CharSequence, width: Int) {
+        val maximumTextWidth = width - (paddingLeft + paddingRight)
+        val readMoreMaxLines = readMoreMaxLines
+        if (maximumTextWidth > 0 && readMoreMaxLines > 0) {
+            val layout = StaticLayoutCompat.Builder(text, paint, maximumTextWidth)
+                .setLineSpacing(lineSpacingExtra, lineSpacingMultiplier)
+                .setIncludePad(includeFontPadding)
+                .build()
+            if (layout.lineCount <= readMoreMaxLines) {
+                this.collapseText = text
+            } else {
+                this.collapseText = buildSpannedString {
+                    val countUntilMaxLine = layout.getLineVisibleEnd(readMoreMaxLines - 1)
+                    if (text.length <= countUntilMaxLine) {
+                        append(text)
+                    } else {
+                        val overflowText = buildOverflowText()
+                        val overflowTextWidth = StaticLayoutCompat.Builder(
+                            overflowText,
+                            paint,
+                            maximumTextWidth
+                        )
+                            .build()
+                            .getLineWidth(0).toInt()
+
+                        val textAppearanceSpan = TextAppearanceSpan(
+                            null,
+                            Typeface.NORMAL,
+                            textSize.toInt(),
+                            readMoreTextColor,
+                            null
+                        )
+                        val clickSpan = object : ClickableSpan() {
+                            override fun onClick(widget: View) {
+                                toggle()
+                            }
+
+                            override fun updateDrawState(ds: TextPaint) {
+                                ds.isUnderlineText = false
+                            }
+
+                        }
+                        val spans = listOfNotNull(
+                            textAppearanceSpan,
+                            clickSpan
+                        )
+                        val readMoreTextWithStyle = buildReadMoreText(spans = spans.toTypedArray())
+                        val readMorePaint = TextPaint().apply {
+                            set(paint)
+                            textAppearanceSpan.updateMeasureState(this)
+                        }
+                        val readMoreTextWidth = StaticLayoutCompat.Builder(
+                            readMoreTextWithStyle,
+                            readMorePaint,
+                            maximumTextWidth
+                        )
+                            .build()
+                            .getLineWidth(0).toInt()
+                        val readMoreWidth = overflowTextWidth + readMoreTextWidth
+
+                        val replaceCount = text
+                            .substringOf(layout, line = readMoreMaxLines)
+                            .calculateReplaceCountToBeSingleLineWith(maximumTextWidth - readMoreWidth)
+                        append(text.subSequence(0, countUntilMaxLine - replaceCount))
+                        append(overflowText)
+                        append(readMoreTextWithStyle)
+                    }
+                }
+            }
+        } else {
+            this.collapseText = text
+        }
+        invalidateText()
+    }
+
+    private fun buildOverflowText(
+        text: String? = readMoreText
+    ): String {
+        return buildString {
+            append(ellipsis)
+            if (text.isNullOrEmpty().not()) append(nbsp)
+        }
+    }
+
+    private fun buildReadMoreText(
+        text: String? = readMoreText,
+        vararg spans: Any
+    ): CharSequence {
+        return buildSpannedString {
+            if (text.isNullOrEmpty().not()) {
+                inSpans(spans = spans) {
+                    append(text)
+                }
+            }
+        }
+    }
+
+    private fun CharSequence.substringOf(layout: Layout, line: Int): CharSequence {
+        val lastLineStartIndex = layout.getLineStart(line - 1)
+        val lastLineEndIndex = layout.getLineEnd(line - 1)
+        return subSequence(lastLineStartIndex, lastLineEndIndex)
+    }
+
+    private fun CharSequence.calculateReplaceCountToBeSingleLineWith(
+        maximumTextWidth: Int
+    ): Int {
+        val currentTextBounds = Rect()
+        var replacedCount = -1
+        do {
+            replacedCount++
+            val replacedText = substring(0, this.length - replacedCount)
+            paint.getTextBounds(replacedText, 0, replacedText.length, currentTextBounds)
+        } while (replacedCount < this.length && currentTextBounds.width() >= maximumTextWidth)
+
+        val lastVisibleChar: Char? = this.getOrNull(this.length - replacedCount - 1)
+        val firstOverflowChar: Char? = this.getOrNull(this.length - replacedCount)
+        if (lastVisibleChar?.isSurrogate() == true && firstOverflowChar?.isHighSurrogate() == false) {
+            val subText = substring(0, this.length - replacedCount)
+            if (subText.isNotEmpty()) {
+                return length - subText.indexOfLast { it.isHighSurrogate() }
+            }
+        }
+        return replacedCount
+    }
+
+    private fun invalidateText() {
+        if (expanded) {
+            super.setText(originalText, bufferType)
+            super.setMaxLines(NO_LIMIT_LINES)
+        } else {
+            super.setText(collapseText, bufferType)
+            super.setMaxLines(readMoreMaxLines)
+        }
+    }
+
+    private companion object {
+        private const val NO_LIMIT_LINES = Integer.MAX_VALUE
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/futo/circles/view/read_more/StaticLayoutCompat.kt b/app/src/main/java/org/futo/circles/view/read_more/StaticLayoutCompat.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4957b065af6426f2c42baa8a49fdde86d0c68b9c
--- /dev/null
+++ b/app/src/main/java/org/futo/circles/view/read_more/StaticLayoutCompat.kt
@@ -0,0 +1,56 @@
+package org.futo.circles.view.read_more
+
+import android.text.Layout
+import android.text.StaticLayout
+import android.text.TextPaint
+import android.text.TextUtils
+import androidx.annotation.FloatRange
+
+internal class StaticLayoutCompat {
+
+    class Builder(
+        private val text: CharSequence,
+        private val start: Int,
+        private val end: Int,
+        private val paint: TextPaint,
+        private val width: Int
+    ) {
+        constructor(text: CharSequence, paint: TextPaint, width: Int) :
+                this(text, 0, text.length, paint, width)
+
+        private var alignment: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL
+        private var spacingMult = 1f
+        private var spacingAdd = 0f
+        private var includePad = true
+        private var ellipsizedWidth = width
+        private var ellipsize: TextUtils.TruncateAt? = null
+        private var maxLines = Integer.MAX_VALUE
+
+
+        fun setLineSpacing(
+            spacingAdd: Float,
+            @FloatRange(from = 0.0) spacingMult: Float
+        ): Builder {
+            this.spacingAdd = spacingAdd
+            this.spacingMult = spacingMult
+            return this
+        }
+
+        fun setIncludePad(includePad: Boolean): Builder {
+            this.includePad = includePad
+            return this
+        }
+
+        fun build(): StaticLayout {
+            return StaticLayout.Builder
+                .obtain(text, start, end, paint, width)
+                .setAlignment(alignment)
+                .setLineSpacing(spacingAdd, spacingMult)
+                .setIncludePad(includePad)
+                .setEllipsize(ellipsize)
+                .setEllipsizedWidth(ellipsizedWidth)
+                .setMaxLines(maxLines)
+                .build()
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_text_post.xml b/app/src/main/res/layout/view_text_post.xml
index 8a245793765501c1f10652079b5178ceaa27687e..fcf69dbe2521502bafab6f69ca2473a3dd391197 100644
--- a/app/src/main/res/layout/view_text_post.xml
+++ b/app/src/main/res/layout/view_text_post.xml
@@ -1,16 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
 <org.futo.circles.view.PostLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/lTextPost"
     android:layout_width="match_parent"
     android:layout_height="wrap_content">
 
-    <TextView
+    <org.futo.circles.view.read_more.ReadMoreTextView
         android:id="@+id/tvContent"
         style="@style/postMessage"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginHorizontal="@dimen/post_text_side_margin"
         android:autoLink="web"
-        android:textIsSelectable="true" />
+        android:textIsSelectable="true"
+        app:readMoreMaxLines="7"
+        app:readMoreText="@string/show_more"
+        app:readMoreTextColor="@color/blue" />
 
 </org.futo.circles.view.PostLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index cf6e865ece5e7da909547051f6c56dbec4bfdd01..288f89e98354e5835b5ad3174892537d4a717c20 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -16,4 +16,11 @@
     <declare-styleable name="GroupPostHeaderView">
         <attr name="optionsVisible" format="boolean" />
     </declare-styleable>
+
+    <declare-styleable name="ReadMoreTextView">
+        <attr name="readMoreMaxLines" format="integer" />
+        <attr name="readMoreText" format="string" />
+        <attr name="readMoreTextColor" format="reference|color" />
+    </declare-styleable>
+    <attr name="readMoreTextViewStyle" format="reference" />
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f426987107791c50305798d275e94842436079f5..4869804d213cd84d69af6322db53d1d5f1dd2d6f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -297,6 +297,7 @@
     <string name="strong_password">Strong password</string>
     <string name="very_strong_password">Very strong password</string>
     <string name="back">Back</string>
+    <string name="show_more">Show more</string>
 
     <string-array name="report_categories">
         <item>@string/crude_language</item>