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,95 @@
|
||||
package com.hootsuite.nachos.terminator;
|
||||
|
||||
import android.text.Editable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.tokenizer.ChipTokenizer;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This interface is used to handle the management of characters that should trigger the creation of chips in a text view.
|
||||
*
|
||||
* @see ChipTokenizer
|
||||
*/
|
||||
public interface ChipTerminatorHandler {
|
||||
|
||||
/**
|
||||
* When a chip terminator character is encountered in newly inserted text, all tokens in the whole text view will be chipified
|
||||
*/
|
||||
int BEHAVIOR_CHIPIFY_ALL = 0;
|
||||
|
||||
/**
|
||||
* When a chip terminator character is encountered in newly inserted text, only the current token (that in which the chip terminator character
|
||||
* was found) will be chipified. This token may extend beyond where the chip terminator character was located.
|
||||
*/
|
||||
int BEHAVIOR_CHIPIFY_CURRENT_TOKEN = 1;
|
||||
|
||||
/**
|
||||
* When a chip terminator character is encountered in newly inserted text, only the text from the previous chip up until the chip terminator
|
||||
* character will be chipified. This may not be an entire token.
|
||||
*/
|
||||
int BEHAVIOR_CHIPIFY_TO_TERMINATOR = 2;
|
||||
|
||||
/**
|
||||
* Constant for use with {@link #setPasteBehavior(int)}. Use this if a paste should behave the same as a standard text input (the chip temrinators
|
||||
* will all behave according to their pre-determined behavior set through {@link #addChipTerminator(char, int)} or {@link #setChipTerminators(Map)}).
|
||||
*/
|
||||
int PASTE_BEHAVIOR_USE_DEFAULT = -1;
|
||||
|
||||
/**
|
||||
* Sets all the characters that will be marked as chip terminators. This will replace any previously set chip terminators.
|
||||
*
|
||||
* @param chipTerminators a map of characters to be marked as chip terminators to behaviors that describe how to respond to the characters, or null
|
||||
* to remove all chip terminators
|
||||
*/
|
||||
void setChipTerminators(@Nullable Map<Character, Integer> chipTerminators);
|
||||
|
||||
/**
|
||||
* Adds a character as a chip terminator. When the provided character is encountered in entered text, the nearby text will be chipified according
|
||||
* to the behavior provided here.
|
||||
* {@code behavior} Must be one of:
|
||||
* <ul>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_ALL}</li>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_CURRENT_TOKEN}</li>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_TO_TERMINATOR}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param character the character to mark as a chip terminator
|
||||
* @param behavior the behavior describing how to respond to the chip terminator
|
||||
*/
|
||||
void addChipTerminator(char character, int behavior);
|
||||
|
||||
/**
|
||||
* Customizes the way paste events are handled.
|
||||
* If one of:
|
||||
* <ul>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_ALL}</li>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_CURRENT_TOKEN}</li>
|
||||
* <li>{@link #BEHAVIOR_CHIPIFY_TO_TERMINATOR}</li>
|
||||
* </ul>
|
||||
* is passed, all chip terminators will be handled with that behavior when a paste event occurs.
|
||||
* If {@link #PASTE_BEHAVIOR_USE_DEFAULT} is passed, whatever behavior is configured for a particular chip terminator
|
||||
* (through {@link #setChipTerminators(Map)} or {@link #addChipTerminator(char, int)} will be used for that chip terminator
|
||||
*
|
||||
* @param pasteBehavior the behavior to use on a paste event
|
||||
*/
|
||||
void setPasteBehavior(int pasteBehavior);
|
||||
|
||||
/**
|
||||
* Parses the provided text looking for characters marked as chip terminators through {@link #addChipTerminator(char, int)} and {@link #setChipTerminators(Map)}.
|
||||
* The provided {@link Editable} will be modified if chip terminators are encountered.
|
||||
*
|
||||
* @param tokenizer the {@link ChipTokenizer} to use to identify and chipify tokens in the text
|
||||
* @param text the text in which to search for chip terminators tokens to be chipped
|
||||
* @param start the index at which to begin looking for chip terminators (inclusive)
|
||||
* @param end the index at which to end looking for chip terminators (exclusive)
|
||||
* @param isPasteEvent true if this handling is for a paste event in which case the behavior set in {@link #setPasteBehavior(int)} will be used,
|
||||
* otherwise false
|
||||
* @return an non-negative integer indicating the index where the cursor (selection) should be placed once the handling is complete,
|
||||
* or a negative integer indicating that the cursor should not be moved.
|
||||
*/
|
||||
int findAndHandleChipTerminators(@NonNull ChipTokenizer tokenizer, @NonNull Editable text, int start, int end, boolean isPasteEvent);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.hootsuite.nachos.terminator;
|
||||
|
||||
import android.text.Editable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hootsuite.nachos.tokenizer.ChipTokenizer;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class DefaultChipTerminatorHandler implements ChipTerminatorHandler {
|
||||
|
||||
@Nullable
|
||||
private Map<Character, Integer> mChipTerminators;
|
||||
private int mPasteBehavior = BEHAVIOR_CHIPIFY_TO_TERMINATOR;
|
||||
|
||||
@Override
|
||||
public void setChipTerminators(@Nullable Map<Character, Integer> chipTerminators) {
|
||||
mChipTerminators = chipTerminators;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChipTerminator(char character, int behavior) {
|
||||
if (mChipTerminators == null) {
|
||||
mChipTerminators = new HashMap<>();
|
||||
}
|
||||
|
||||
mChipTerminators.put(character, behavior);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPasteBehavior(int pasteBehavior) {
|
||||
mPasteBehavior = pasteBehavior;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findAndHandleChipTerminators(@NonNull ChipTokenizer tokenizer, @NonNull Editable text, int start, int end, boolean isPasteEvent) {
|
||||
// If we don't have a tokenizer or any chip terminators, there's nothing to look for
|
||||
if (mChipTerminators == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
TextIterator textIterator = new TextIterator(text, start, end);
|
||||
int selectionIndex = -1;
|
||||
|
||||
characterLoop:
|
||||
while (textIterator.hasNextCharacter()) {
|
||||
char theChar = textIterator.nextCharacter();
|
||||
if (isChipTerminator(theChar)) {
|
||||
int behavior = (isPasteEvent && mPasteBehavior != PASTE_BEHAVIOR_USE_DEFAULT) ? mPasteBehavior : mChipTerminators.get(theChar);
|
||||
int newSelection = -1;
|
||||
switch (behavior) {
|
||||
case BEHAVIOR_CHIPIFY_ALL:
|
||||
selectionIndex = handleChipifyAll(textIterator, tokenizer);
|
||||
break characterLoop;
|
||||
case BEHAVIOR_CHIPIFY_CURRENT_TOKEN:
|
||||
newSelection = handleChipifyCurrentToken(textIterator, tokenizer);
|
||||
break;
|
||||
case BEHAVIOR_CHIPIFY_TO_TERMINATOR:
|
||||
newSelection = handleChipifyToTerminator(textIterator, tokenizer);
|
||||
break;
|
||||
}
|
||||
|
||||
if (newSelection != -1) {
|
||||
selectionIndex = newSelection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selectionIndex;
|
||||
}
|
||||
|
||||
private int handleChipifyAll(TextIterator textIterator, ChipTokenizer tokenizer) {
|
||||
textIterator.deleteCharacter(true);
|
||||
tokenizer.terminateAllTokens(textIterator.getText());
|
||||
return textIterator.totalLength();
|
||||
}
|
||||
|
||||
private int handleChipifyCurrentToken(TextIterator textIterator, ChipTokenizer tokenizer) {
|
||||
textIterator.deleteCharacter(true);
|
||||
Editable text = textIterator.getText();
|
||||
int index = textIterator.getIndex();
|
||||
int tokenStart = tokenizer.findTokenStart(text, index);
|
||||
int tokenEnd = tokenizer.findTokenEnd(text, index);
|
||||
if (tokenStart < tokenEnd) {
|
||||
CharSequence chippedText = tokenizer.terminateToken(text.subSequence(tokenStart, tokenEnd), null);
|
||||
textIterator.replace(tokenStart, tokenEnd, chippedText);
|
||||
return tokenStart + chippedText.length();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int handleChipifyToTerminator(TextIterator textIterator, ChipTokenizer tokenizer) {
|
||||
Editable text = textIterator.getText();
|
||||
int index = textIterator.getIndex();
|
||||
if (index > 0) {
|
||||
int tokenStart = tokenizer.findTokenStart(text, index);
|
||||
if (tokenStart < index) {
|
||||
CharSequence chippedText = tokenizer.terminateToken(text.subSequence(tokenStart, index), null);
|
||||
textIterator.replace(tokenStart, index + 1, chippedText);
|
||||
} else {
|
||||
textIterator.deleteCharacter(false);
|
||||
}
|
||||
} else {
|
||||
textIterator.deleteCharacter(false);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private boolean isChipTerminator(char character) {
|
||||
return mChipTerminators != null && mChipTerminators.keySet().contains(character);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.hootsuite.nachos.terminator;
|
||||
|
||||
import android.text.Editable;
|
||||
|
||||
public class TextIterator {
|
||||
|
||||
private Editable mText;
|
||||
private int mStart;
|
||||
private int mEnd;
|
||||
|
||||
private int mIndex;
|
||||
|
||||
public TextIterator(Editable text, int start, int end) {
|
||||
mText = text;
|
||||
mStart = start;
|
||||
mEnd = end;
|
||||
|
||||
mIndex = mStart - 1; // Subtract 1 so that the first call to nextCharacter() will return the first character
|
||||
}
|
||||
|
||||
public int totalLength() {
|
||||
return mText.length();
|
||||
}
|
||||
|
||||
public int windowLength() {
|
||||
return mEnd - mStart;
|
||||
}
|
||||
|
||||
public Editable getText() {
|
||||
return mText;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return mIndex;
|
||||
}
|
||||
|
||||
public boolean hasNextCharacter() {
|
||||
return (mIndex + 1) < mEnd;
|
||||
}
|
||||
|
||||
public char nextCharacter() {
|
||||
mIndex++;
|
||||
return mText.charAt(mIndex);
|
||||
}
|
||||
|
||||
public void deleteCharacter(boolean maintainIndex) {
|
||||
mText.replace(mIndex, mIndex + 1, "");
|
||||
if (!maintainIndex) {
|
||||
mIndex--;
|
||||
}
|
||||
mEnd--;
|
||||
}
|
||||
|
||||
public void replace(int replaceStart, int replaceEnd, CharSequence chippedText) {
|
||||
mText.replace(replaceStart, replaceEnd, chippedText);
|
||||
|
||||
// Update indexes
|
||||
int newLength = chippedText.length();
|
||||
int oldLength = replaceEnd - replaceStart;
|
||||
mIndex = replaceStart + newLength - 1;
|
||||
mEnd += newLength - oldLength;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user