Hashtag timelines with multiple tags (#584)
* feat(api/hashtag): add any, all, and none parameter * feat(timeline/hashtag): load with any, all and none parameter * feat(timeline/hashtag): save any, all and none in timeline definition * feat: set hastag parameter in UI * feat: move strings to string res * feat: show hint for tags * refactor: use method for setting up tags text * improve edit dialog, allow creating hashtag timelines * add chips for hashtags * add option for displaying only local posts in hashtag * improve layout and wording --------- Co-authored-by: sk <sk22@mailbox.org>
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
package com.hootsuite.nachos.tokenizer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.ChipConfiguration;
|
||||
import com.hootsuite.nachos.chip.Chip;
|
||||
import com.hootsuite.nachos.chip.ChipCreator;
|
||||
import com.hootsuite.nachos.chip.ChipSpan;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link ChipTokenizer}.
|
||||
* This implementation does the following:
|
||||
* <ul>
|
||||
* <li>Surrounds each token with a space and the Unit Separator ASCII control character (31) - See the diagram below
|
||||
* <ul>
|
||||
* <li>The spaces are included so that android keyboards can distinguish the chips as different words and provide accurate
|
||||
* autocorrect suggestions</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>Replaces each token with a {@link ChipSpan} containing the same text, once the token terminates</li>
|
||||
* <li>Uses the values passed to {@link #applyConfiguration(Editable, ChipConfiguration)} to configure any ChipSpans that get created</li>
|
||||
* </ul>
|
||||
* Each terminated token will therefore look like the following (this is what will be returned from {@link #terminateToken(CharSequence, Object)}):
|
||||
* <pre>
|
||||
* -----------------------------------------------------------
|
||||
* | SpannableString |
|
||||
* | ---------------------------------------------------- |
|
||||
* | | ChipSpan | |
|
||||
* | | | |
|
||||
* | | space separator text separator space | |
|
||||
* | | | |
|
||||
* | ---------------------------------------------------- |
|
||||
* -----------------------------------------------------------
|
||||
* </pre>
|
||||
*
|
||||
* @see ChipSpan
|
||||
*/
|
||||
public class SpanChipTokenizer<C extends Chip> implements ChipTokenizer {
|
||||
|
||||
/**
|
||||
* The character used to separate chips internally is the US (Unit Separator) ASCII control character.
|
||||
* This character is used because it's untypable so we have complete control over when chips are created.
|
||||
*/
|
||||
public static final char CHIP_SPAN_SEPARATOR = 31;
|
||||
public static final char AUTOCORRECT_SEPARATOR = ' ';
|
||||
|
||||
private Context mContext;
|
||||
|
||||
@Nullable
|
||||
private ChipConfiguration mChipConfiguration;
|
||||
@NonNull
|
||||
private ChipCreator<C> mChipCreator;
|
||||
@NonNull
|
||||
private Class<C> mChipClass;
|
||||
|
||||
private Comparator<Pair<Integer, Integer>> mReverseTokenIndexesSorter = new Comparator<Pair<Integer, Integer>>() {
|
||||
@Override
|
||||
public int compare(Pair<Integer, Integer> lhs, Pair<Integer, Integer> rhs) {
|
||||
return rhs.first - lhs.first;
|
||||
}
|
||||
};
|
||||
|
||||
public SpanChipTokenizer(Context context, @NonNull ChipCreator<C> chipCreator, @NonNull Class<C> chipClass) {
|
||||
mContext = context;
|
||||
mChipCreator = chipCreator;
|
||||
mChipClass = chipClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyConfiguration(Editable text, ChipConfiguration chipConfiguration) {
|
||||
mChipConfiguration = chipConfiguration;
|
||||
|
||||
for (C chip : findAllChips(0, text.length(), text)) {
|
||||
// Recreate the chips with the new configuration
|
||||
int chipStart = findChipStart(chip, text);
|
||||
deleteChip(chip, text);
|
||||
text.insert(chipStart, terminateToken(mChipCreator.createChip(mContext, chip)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findTokenStart(CharSequence text, int cursor) {
|
||||
int i = cursor;
|
||||
|
||||
// Work backwards until we find a CHIP_SPAN_SEPARATOR
|
||||
while (i > 0 && text.charAt(i - 1) != CHIP_SPAN_SEPARATOR) {
|
||||
i--;
|
||||
}
|
||||
// Work forwards to skip over any extra whitespace at the beginning of the token
|
||||
while (i > 0 && i < text.length() && Character.isWhitespace(text.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findTokenEnd(CharSequence text, int cursor) {
|
||||
int i = cursor;
|
||||
int len = text.length();
|
||||
|
||||
// Work forwards till we find a CHIP_SPAN_SEPARATOR
|
||||
while (i < len) {
|
||||
if (text.charAt(i) == CHIP_SPAN_SEPARATOR) {
|
||||
return (i - 1); // subtract one because the CHIP_SPAN_SEPARATOR will be preceded by a space
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Pair<Integer, Integer>> findAllTokens(CharSequence text) {
|
||||
List<Pair<Integer, Integer>> unterminatedTokens = new ArrayList<>();
|
||||
|
||||
boolean insideChip = false;
|
||||
// Iterate backwards through the text (to avoid messing up indexes)
|
||||
for (int index = text.length() - 1; index >= 0; index--) {
|
||||
char theCharacter = text.charAt(index);
|
||||
|
||||
// Every time we hit a CHIP_SPAN_SEPARATOR character we switch from being inside to outside
|
||||
// or outside to inside a chip
|
||||
// This check must happen before the whitespace check because CHIP_SPAN_SEPARATOR is considered a whitespace character
|
||||
if (theCharacter == CHIP_SPAN_SEPARATOR) {
|
||||
insideChip = !insideChip;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Completely skip over whitespace
|
||||
if (Character.isWhitespace(theCharacter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're ever outside a chip, see if the text we're in is a viable token for chipification
|
||||
if (!insideChip) {
|
||||
int tokenStart = findTokenStart(text, index);
|
||||
int tokenEnd = findTokenEnd(text, index);
|
||||
|
||||
// Can only actually be chipified if there's at least one character between them
|
||||
if (tokenEnd - tokenStart >= 1) {
|
||||
unterminatedTokens.add(new Pair<>(tokenStart, tokenEnd));
|
||||
index = tokenStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
return unterminatedTokens;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence terminateToken(CharSequence text, @Nullable Object data) {
|
||||
// Remove leading/trailing whitespace
|
||||
CharSequence trimmedText = text.toString().trim();
|
||||
return terminateToken(mChipCreator.createChip(mContext, trimmedText, data));
|
||||
}
|
||||
|
||||
private CharSequence terminateToken(C chip) {
|
||||
// Surround the text with CHIP_SPAN_SEPARATOR and spaces
|
||||
// The spaces allow autocorrect to correctly identify words
|
||||
String chipSeparator = Character.toString(CHIP_SPAN_SEPARATOR);
|
||||
String autoCorrectSeparator = Character.toString(AUTOCORRECT_SEPARATOR);
|
||||
CharSequence textWithSeparator = autoCorrectSeparator + chipSeparator + chip.getText() + chipSeparator + autoCorrectSeparator;
|
||||
|
||||
// Build the container object to house the ChipSpan and space
|
||||
SpannableString spannableString = new SpannableString(textWithSeparator);
|
||||
|
||||
// Attach the ChipSpan
|
||||
if (mChipConfiguration != null) {
|
||||
mChipCreator.configureChip(chip, mChipConfiguration);
|
||||
}
|
||||
spannableString.setSpan(chip, 0, textWithSeparator.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
return spannableString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateAllTokens(Editable text) {
|
||||
List<Pair<Integer, Integer>> unterminatedTokens = findAllTokens(text);
|
||||
// Sort in reverse order (so index changes don't affect anything)
|
||||
Collections.sort(unterminatedTokens, mReverseTokenIndexesSorter);
|
||||
for (Pair<Integer, Integer> indexes : unterminatedTokens) {
|
||||
int start = indexes.first;
|
||||
int end = indexes.second;
|
||||
CharSequence textToChip = text.subSequence(start, end);
|
||||
CharSequence chippedText = terminateToken(textToChip, null);
|
||||
text.replace(start, end, chippedText);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findChipStart(Chip chip, Spanned text) {
|
||||
return text.getSpanStart(chip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findChipEnd(Chip chip, Spanned text) {
|
||||
return text.getSpanEnd(chip);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NonNull
|
||||
@Override
|
||||
public C[] findAllChips(int start, int end, Spanned text) {
|
||||
C[] spansArray = text.getSpans(start, end, mChipClass);
|
||||
return (spansArray != null) ? spansArray : (C[]) Array.newInstance(mChipClass, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revertChipToToken(Chip chip, Editable text) {
|
||||
int chipStart = findChipStart(chip, text);
|
||||
int chipEnd = findChipEnd(chip, text);
|
||||
text.removeSpan(chip);
|
||||
text.replace(chipStart, chipEnd, chip.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChip(Chip chip, Editable text) {
|
||||
int chipStart = findChipStart(chip, text);
|
||||
int chipEnd = findChipEnd(chip, text);
|
||||
text.removeSpan(chip);
|
||||
// On the emulator for some reason the text automatically gets deleted and chipStart and chipEnd end up both being -1, so in that case we
|
||||
// don't need to call text.delete(...)
|
||||
if (chipStart != chipEnd) {
|
||||
text.delete(chipStart, chipEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChipAndPadding(Chip chip, Editable text) {
|
||||
// This implementation does not add any extra padding outside of the span so we can just delete the chip normally
|
||||
deleteChip(chip, text);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user