Tusky-App-Android/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt

112 lines
4.6 KiB
Kotlin

/* Copyright 2019 Tusky contributors
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util
import android.text.InputFilter
import android.text.SpannableStringBuilder
import android.text.Spanned
/**
* Defines how many characters to extend beyond the limit to cut at the end of the word on the
* boundary of it rather than cutting at the word preceding that one.
*/
private const val RUNWAY = 10
/**
* Default for maximum status length on Mastodon and default collapsing length on Pleroma.
*/
private const val LENGTH_DEFAULT = 500
/**
* Calculates if it's worth trimming the message at a specific limit or if the content that will
* be hidden will not be enough to justify the operation.
*
* @param message The message to trim.
* @return Whether the message should be trimmed or not.
*/
fun shouldTrimStatus(message: Spanned): Boolean {
return message.isNotEmpty() && LENGTH_DEFAULT.toFloat() / message.length < 0.75
}
/**
* A customized version of {@link android.text.InputFilter.LengthFilter} which allows smarter
* constraints and adds better visuals such as:
* <ul>
* <li>Ellipsis at the end of the constrained text to show continuation.</li>
* <li>Trimming of invisible characters (new lines, spaces, etc.) from the constrained text.</li>
* <li>Constraints end at the end of the last "word", before a whitespace.</li>
* <li>Expansion of the limit by up to 10 characters to facilitate the previous constraint.</li>
* <li>Constraints are not applied if the percentage of hidden content is too small.</li>
* </ul>
*/
object SmartLengthInputFilter : InputFilter {
/** {@inheritDoc} */
override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
// Code originally imported from InputFilter.LengthFilter but heavily customized and converted to Kotlin.
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175
val sourceLength = source.length
var keep = LENGTH_DEFAULT - (dest.length - (dend - dstart))
if (keep <= 0) return ""
if (keep >= end - start) return null // Keep original
keep += start
// Skip trimming if the ratio doesn't warrant it
if (keep.toDouble() / sourceLength > 0.75) return null
// Enable trimming at the end of the closest word if possible
if (source[keep].isLetterOrDigit()) {
var boundary: Int
// Android N+ offer a clone of the ICU APIs in Java for better internationalization and
// unicode support. Using the ICU version of BreakIterator grants better support for
// those without having to add the ICU4J library at a minimum Api trade-off.
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
val iterator = android.icu.text.BreakIterator.getWordInstance()
iterator.setText(source.toString())
boundary = iterator.following(keep)
if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep)
} else {
val iterator = java.text.BreakIterator.getWordInstance()
iterator.setText(source.toString())
boundary = iterator.following(keep)
if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep)
}
keep = boundary
} else {
// If no runway is allowed simply remove whitespace if present
while (source[keep - 1].isWhitespace()) {
--keep
if (keep == start) return ""
}
}
if (source[keep - 1].isHighSurrogate()) {
--keep
if (keep == start) return ""
}
return if (source is Spanned) {
SpannableStringBuilder(source, start, keep).append("")
} else {
"${source.subSequence(start, keep)}"
}
}
}