diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index 989b02df..71010b5c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -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 ) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index b7c5e1e1..52563375 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -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); diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.java b/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.java deleted file mode 100644 index 41400b25..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.java +++ /dev/null @@ -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: - * - * - * 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) + "…"; - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt b/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt new file mode 100644 index 00000000..82d13d87 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt @@ -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 . */ + +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: + * + */ +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)}…" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusExtensions.kt index e0cc7b49..0c4b5e79 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusExtensions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusExtensions.kt @@ -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) } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt index 148461e8..f7353b0a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -310,7 +310,7 @@ class StatusViewHelper(private val itemView: View) { } companion object { - val COLLAPSE_INPUT_FILTER = arrayOf(SmartLengthInputFilter.INSTANCE) + val COLLAPSE_INPUT_FILTER = arrayOf(SmartLengthInputFilter) val NO_INPUT_FILTER = arrayOfNulls(0) } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java index 0ca42c10..ebd56eab 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java @@ -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())