Transfer SmartLengthInputFilter license to Tusky (#1384)
* Transfer SmartLengthInputFilter in-header license Transfer license for file "SmartLengthInputFilter.java" from me (Diego Rossi) to Tusky and therefore also change it from the original Apache 2.0 to currently GPLv3. This was a mistake that has been sitting around for way too long. * Rewrite SmartLengthInputFilter from Java to Kotlin This has been done by hand, without the custom copy-paste tool. * Fix bad references in Java files using SmartLengthInputFilter features * Shorten code in Java classes referencing SmartLengthInputFilter instance * Refactor SmartLengthInputFilter from class to singleton Kotlin object * Move hasBadRatio to become a toplevel function * Patch up all the files affected by SmartLengthInputFilter changes * Length in SmartLengthInputFilter is const 500, simplify code accordingly * More meaningful name for toplevel function for checking trimming ability * Add missing license header
This commit is contained in:
parent
3b1288e99c
commit
87285ae5bf
@ -22,7 +22,7 @@ import androidx.room.Entity
|
||||
import androidx.room.TypeConverters
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.entity.*
|
||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
import java.util.*
|
||||
|
||||
@Entity(primaryKeys = ["id","accountId"])
|
||||
@ -176,7 +176,7 @@ fun Status.toEntity() =
|
||||
spoilerText, attachments, mentions,
|
||||
false,
|
||||
false,
|
||||
!SmartLengthInputFilter.hasBadRatio(content, SmartLengthInputFilter.LENGTH_DEFAULT),
|
||||
!shouldTrimStatus(content),
|
||||
true,
|
||||
poll
|
||||
)
|
||||
|
@ -48,7 +48,7 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
|
||||
import com.keylesspalace.tusky.util.PairedList;
|
||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
||||
import com.keylesspalace.tusky.util.SmartLengthInputFilterKt;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.util.ViewDataUtils;
|
||||
import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
|
||||
@ -360,10 +360,7 @@ public final class ViewThreadFragment extends SFragment implements
|
||||
}
|
||||
|
||||
StatusViewData.Concrete updatedStatus = new StatusViewData.Builder(status)
|
||||
.setCollapsible(!SmartLengthInputFilter.hasBadRatio(
|
||||
status.getContent(),
|
||||
SmartLengthInputFilter.LENGTH_DEFAULT
|
||||
))
|
||||
.setCollapsible(!SmartLengthInputFilterKt.shouldTrimStatus(status.getContent()))
|
||||
.setCollapsed(isCollapsed)
|
||||
.createStatusViewData();
|
||||
statuses.setPairedItem(position, updatedStatus);
|
||||
|
@ -1,154 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Diego Rossi (@_HellPie)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.text.InputFilter;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*
|
||||
* Some of these features are configurable through at instancing time.
|
||||
*/
|
||||
public class SmartLengthInputFilter implements InputFilter {
|
||||
|
||||
/**
|
||||
* 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 static final int RUNWAY = 10;
|
||||
|
||||
/**
|
||||
* Default for maximum status length on Mastodon and default collapsing length on Pleroma.
|
||||
*/
|
||||
public static final int LENGTH_DEFAULT = 500;
|
||||
|
||||
/**
|
||||
* Stores a reusable singleton instance of a {@link SmartLengthInputFilter} already configured
|
||||
* to the default maximum length of {@value #LENGTH_DEFAULT}.
|
||||
*/
|
||||
public static final SmartLengthInputFilter INSTANCE = new SmartLengthInputFilter(LENGTH_DEFAULT);
|
||||
|
||||
private final int max;
|
||||
private final boolean allowRunway;
|
||||
private final boolean skipIfBadRatio;
|
||||
|
||||
/**
|
||||
* Creates a new {@link SmartLengthInputFilter} instance with a predefined maximum length and
|
||||
* all the smart constraint features this class supports.
|
||||
*
|
||||
* @param max The maximum length before trimming. May change based on other constraints.
|
||||
*/
|
||||
public SmartLengthInputFilter(int max) {
|
||||
this(max, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully configures a new {@link SmartLengthInputFilter} to fine tune the state of the supported
|
||||
* smart constraints this class supports.
|
||||
*
|
||||
* @param max The maximum length before trimming.
|
||||
* @param allowRunway Whether to extend {@param max} by an extra 10 characters
|
||||
* and trim precisely at the end of the closest word.
|
||||
* @param skipIfBadRatio Whether to skip trimming entirely if the trimmed content
|
||||
* will be less than 25% of the shown content.
|
||||
*/
|
||||
public SmartLengthInputFilter(int max, boolean allowRunway, boolean skipIfBadRatio) {
|
||||
this.max = max;
|
||||
this.allowRunway = allowRunway;
|
||||
this.skipIfBadRatio = skipIfBadRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param limit The maximum length after trimming.
|
||||
* @return Whether the message should be trimmed or not.
|
||||
*/
|
||||
public static boolean hasBadRatio(Spanned message, int limit) {
|
||||
return (double) limit / message.length() > 0.75;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
|
||||
// Code originally imported from InputFilter.LengthFilter but heavily customized.
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175
|
||||
|
||||
int sourceLength = source.length();
|
||||
int keep = max - (dest.length() - (dend - dstart));
|
||||
if (keep <= 0) return "";
|
||||
if (keep >= end - start) return null; // keep original
|
||||
|
||||
keep += start;
|
||||
|
||||
// Enable skipping trimming if the ratio is not good enough
|
||||
if (skipIfBadRatio && (double)keep / sourceLength > 0.75)
|
||||
return null;
|
||||
|
||||
// Enable trimming at the end of the closest word if possible
|
||||
if (allowRunway && Character.isLetterOrDigit(source.charAt(keep))) {
|
||||
int boundary;
|
||||
|
||||
// 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) {
|
||||
android.icu.text.BreakIterator iterator = android.icu.text.BreakIterator.getWordInstance();
|
||||
iterator.setText(source.toString());
|
||||
boundary = iterator.following(keep);
|
||||
if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep);
|
||||
} else {
|
||||
java.text.BreakIterator 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 whitespaces if present
|
||||
while (Character.isWhitespace(source.charAt(keep - 1))) {
|
||||
--keep;
|
||||
if (keep == start) return "";
|
||||
}
|
||||
}
|
||||
|
||||
if (Character.isHighSurrogate(source.charAt(keep - 1))) {
|
||||
--keep;
|
||||
if (keep == start) return "";
|
||||
}
|
||||
|
||||
if (source instanceof Spanned) {
|
||||
return new SpannableStringBuilder(source, start, keep).append("…");
|
||||
} else {
|
||||
return source.subSequence(start, keep) + "…";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
/* 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 LENGTH_DEFAULT / 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 whitespaces 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)}…"
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,6 @@ package com.keylesspalace.tusky.util
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
||||
fun Status.isCollapsible(): Boolean {
|
||||
return !SmartLengthInputFilter.hasBadRatio(content, SmartLengthInputFilter.LENGTH_DEFAULT)
|
||||
return !shouldTrimStatus(content)
|
||||
}
|
||||
|
||||
|
@ -310,7 +310,7 @@ class StatusViewHelper(private val itemView: View) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val COLLAPSE_INPUT_FILTER = arrayOf<InputFilter>(SmartLengthInputFilter.INSTANCE)
|
||||
val COLLAPSE_INPUT_FILTER = arrayOf<InputFilter>(SmartLengthInputFilter)
|
||||
val NO_INPUT_FILTER = arrayOfNulls<InputFilter>(0)
|
||||
}
|
||||
}
|
@ -58,10 +58,7 @@ public final class ViewDataUtils {
|
||||
.setApplication(visibleStatus.getApplication())
|
||||
.setStatusEmojis(visibleStatus.getEmojis())
|
||||
.setAccountEmojis(visibleStatus.getAccount().getEmojis())
|
||||
.setCollapsible(!SmartLengthInputFilter.hasBadRatio(
|
||||
visibleStatus.getContent(),
|
||||
SmartLengthInputFilter.LENGTH_DEFAULT
|
||||
))
|
||||
.setCollapsible(!SmartLengthInputFilterKt.shouldTrimStatus(visibleStatus.getContent()))
|
||||
.setCollapsed(true)
|
||||
.setPoll(visibleStatus.getPoll())
|
||||
.setCard(visibleStatus.getCard())
|
||||
|
Loading…
Reference in New Issue
Block a user