improve replies
This commit is contained in:
parent
b1c3001a97
commit
5bb72ec049
96
src/main/java/com/cheogram/android/SwipeDetector.java
Normal file
96
src/main/java/com/cheogram/android/SwipeDetector.java
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package com.cheogram.android;
|
||||||
|
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewConfiguration;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.utils.Consumer;
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/41766670/8611
|
||||||
|
/**
|
||||||
|
* Created by hoshyar on 1/19/17.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class SwipeDetector implements View.OnTouchListener {
|
||||||
|
|
||||||
|
protected Consumer<Action> cb;
|
||||||
|
|
||||||
|
private Set<Action> allowedActions;
|
||||||
|
|
||||||
|
private int touchSlop = -1;
|
||||||
|
|
||||||
|
public SwipeDetector(Consumer<Action> cb, Set<Action> allowedActions) {
|
||||||
|
this.cb = cb;
|
||||||
|
this.allowedActions = allowedActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static enum Action {
|
||||||
|
LR, // Left to Right
|
||||||
|
RL, // Right to Left
|
||||||
|
None // when no action was detected
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String logTag = "Swipe";
|
||||||
|
private static final int MIN_DISTANCE = 100;
|
||||||
|
private float downX, downY, upX, upY;
|
||||||
|
private Action mSwipeDetected = Action.None;
|
||||||
|
|
||||||
|
public boolean swipeDetected() {
|
||||||
|
return mSwipeDetected != Action.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action getAction() {
|
||||||
|
return mSwipeDetected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouch(View v, MotionEvent event) {
|
||||||
|
if (touchSlop == -1) {
|
||||||
|
touchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop();
|
||||||
|
android.util.Log.e("25fd", touchSlop + " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
downX = event.getX();
|
||||||
|
downY = event.getY();
|
||||||
|
mSwipeDetected = Action.None;
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
upX = event.getX();
|
||||||
|
upY = event.getY();
|
||||||
|
|
||||||
|
float deltaX = downX - upX;
|
||||||
|
float deltaY = downY - upY;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(allowedActions.contains(Action.LR) && deltaX > touchSlop ||
|
||||||
|
allowedActions.contains(Action.RL) && deltaX < -touchSlop) && Math.abs(deltaY) < touchSlop
|
||||||
|
) {
|
||||||
|
v.getParent().requestDisallowInterceptTouchEvent(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(deltaX) > dpToPx(MIN_DISTANCE)) {
|
||||||
|
// left or right
|
||||||
|
if (deltaX < 0 && allowedActions.contains(Action.LR)) {
|
||||||
|
cb.accept(mSwipeDetected = Action.LR);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (deltaX > 0 && allowedActions.contains(Action.RL)) {
|
||||||
|
cb.accept(mSwipeDetected = Action.RL);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int dpToPx(int dp) {
|
||||||
|
return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
}
|
|
@ -84,6 +84,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
|
private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
|
||||||
private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
|
private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
|
||||||
private String mFirstMamReference = null;
|
private String mFirstMamReference = null;
|
||||||
|
protected Message replyTo = null;
|
||||||
|
|
||||||
public Conversation(final String name, final Account account, final Jid contactJid,
|
public Conversation(final String name, final Account account, final Jid contactJid,
|
||||||
final int mode) {
|
final int mode) {
|
||||||
|
@ -559,6 +560,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
this.draftMessage = draftMessage;
|
this.draftMessage = draftMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setReplyTo(Message m) {
|
||||||
|
this.replyTo = m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message getReplyTo() {
|
||||||
|
return this.replyTo;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isRead() {
|
public boolean isRead() {
|
||||||
synchronized (this.messages) {
|
synchronized (this.messages) {
|
||||||
for(final Message message : Lists.reverse(this.messages)) {
|
for(final Message message : Lists.reverse(this.messages)) {
|
||||||
|
|
|
@ -44,7 +44,7 @@ public class IndividualMessage extends Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, String edited, boolean oob, String errorMessage, Set<ReadByMarker> readByMarkers, boolean markable, boolean deleted, String bodyLanguage) {
|
private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, String edited, boolean oob, String errorMessage, Set<ReadByMarker> readByMarkers, boolean markable, boolean deleted, String bodyLanguage) {
|
||||||
super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable, deleted, bodyLanguage);
|
super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable, deleted, bodyLanguage, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -5,13 +5,14 @@ import android.database.Cursor;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.io.ByteSource;
|
||||||
import com.google.common.primitives.Longs;
|
import com.google.common.primitives.Longs;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -26,12 +27,16 @@ import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
||||||
import eu.siacs.conversations.http.URL;
|
import eu.siacs.conversations.http.URL;
|
||||||
import eu.siacs.conversations.services.AvatarService;
|
import eu.siacs.conversations.services.AvatarService;
|
||||||
import eu.siacs.conversations.ui.util.PresenceSelector;
|
import eu.siacs.conversations.ui.util.PresenceSelector;
|
||||||
|
import eu.siacs.conversations.ui.util.QuoteHelper;
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
import eu.siacs.conversations.utils.Emoticons;
|
import eu.siacs.conversations.utils.Emoticons;
|
||||||
import eu.siacs.conversations.utils.GeoHelper;
|
import eu.siacs.conversations.utils.GeoHelper;
|
||||||
import eu.siacs.conversations.utils.MessageUtils;
|
import eu.siacs.conversations.utils.MessageUtils;
|
||||||
import eu.siacs.conversations.utils.MimeUtils;
|
import eu.siacs.conversations.utils.MimeUtils;
|
||||||
import eu.siacs.conversations.utils.UIHelper;
|
import eu.siacs.conversations.utils.UIHelper;
|
||||||
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
import eu.siacs.conversations.xml.Tag;
|
||||||
|
import eu.siacs.conversations.xml.XmlReader;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
|
||||||
public class Message extends AbstractEntity implements AvatarService.Avatarable {
|
public class Message extends AbstractEntity implements AvatarService.Avatarable {
|
||||||
|
@ -85,6 +90,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
public static final String READ_BY_MARKERS = "readByMarkers";
|
public static final String READ_BY_MARKERS = "readByMarkers";
|
||||||
public static final String MARKABLE = "markable";
|
public static final String MARKABLE = "markable";
|
||||||
public static final String DELETED = "deleted";
|
public static final String DELETED = "deleted";
|
||||||
|
|
||||||
|
public static final String PAYLOADS = "payloads";
|
||||||
public static final String ME_COMMAND = "/me ";
|
public static final String ME_COMMAND = "/me ";
|
||||||
|
|
||||||
public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
|
public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
|
||||||
|
@ -103,6 +110,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
protected boolean deleted = false;
|
protected boolean deleted = false;
|
||||||
protected boolean carbon = false;
|
protected boolean carbon = false;
|
||||||
protected boolean oob = false;
|
protected boolean oob = false;
|
||||||
|
protected List<Element> payloads = new ArrayList<>();
|
||||||
protected List<Edit> edits = new ArrayList<>();
|
protected List<Edit> edits = new ArrayList<>();
|
||||||
protected String relativeFilePath;
|
protected String relativeFilePath;
|
||||||
protected boolean read = true;
|
protected boolean read = true;
|
||||||
|
@ -154,6 +162,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
null,
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,6 +188,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
null,
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +198,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
final String remoteMsgId, final String relativeFilePath,
|
final String remoteMsgId, final String relativeFilePath,
|
||||||
final String serverMsgId, final String fingerprint, final boolean read,
|
final String serverMsgId, final String fingerprint, final boolean read,
|
||||||
final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
|
final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
|
||||||
final boolean markable, final boolean deleted, final String bodyLanguage) {
|
final boolean markable, final boolean deleted, final String bodyLanguage, final List<Element> payloads) {
|
||||||
this.conversation = conversation;
|
this.conversation = conversation;
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.conversationUuid = conversationUUid;
|
this.conversationUuid = conversationUUid;
|
||||||
|
@ -212,9 +222,21 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
this.markable = markable;
|
this.markable = markable;
|
||||||
this.deleted = deleted;
|
this.deleted = deleted;
|
||||||
this.bodyLanguage = bodyLanguage;
|
this.bodyLanguage = bodyLanguage;
|
||||||
|
if (payloads != null) this.payloads = payloads;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Message fromCursor(Cursor cursor, Conversation conversation) {
|
public static Message fromCursor(Cursor cursor, Conversation conversation) throws IOException {
|
||||||
|
String payloadsStr = cursor.getString(cursor.getColumnIndex(PAYLOADS));
|
||||||
|
List<Element> payloads = new ArrayList<>();
|
||||||
|
if (payloadsStr != null) {
|
||||||
|
final XmlReader xmlReader = new XmlReader();
|
||||||
|
xmlReader.setInputStream(ByteSource.wrap(payloadsStr.getBytes()).openStream());
|
||||||
|
Tag tag;
|
||||||
|
while ((tag = xmlReader.readTag()) != null) {
|
||||||
|
payloads.add(xmlReader.readElement(tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new Message(conversation,
|
return new Message(conversation,
|
||||||
cursor.getString(cursor.getColumnIndex(UUID)),
|
cursor.getString(cursor.getColumnIndex(UUID)),
|
||||||
cursor.getString(cursor.getColumnIndex(CONVERSATION)),
|
cursor.getString(cursor.getColumnIndex(CONVERSATION)),
|
||||||
|
@ -237,7 +259,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
|
ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
|
||||||
cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
|
cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
|
||||||
cursor.getInt(cursor.getColumnIndex(DELETED)) > 0,
|
cursor.getInt(cursor.getColumnIndex(DELETED)) > 0,
|
||||||
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE))
|
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)),
|
||||||
|
payloads
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,9 +327,53 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
values.put(MARKABLE, markable ? 1 : 0);
|
values.put(MARKABLE, markable ? 1 : 0);
|
||||||
values.put(DELETED, deleted ? 1 : 0);
|
values.put(DELETED, deleted ? 1 : 0);
|
||||||
values.put(BODY_LANGUAGE, bodyLanguage);
|
values.put(BODY_LANGUAGE, bodyLanguage);
|
||||||
|
|
||||||
|
StringBuilder payloadsValue = null;
|
||||||
|
|
||||||
|
for (Element element : payloads) {
|
||||||
|
if (payloadsValue == null) {
|
||||||
|
payloadsValue = new StringBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadsValue.append(element.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
values.put(PAYLOADS, payloads.size() < 1 ? null : payloadsValue.toString());
|
||||||
|
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String replyId() {
|
||||||
|
return conversation.getMode() == Conversation.MODE_MULTI ? getServerMsgId() : getRemoteMsgId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message reply() {
|
||||||
|
Message m = new Message(conversation, QuoteHelper.quote(MessageUtils.prepareQuote(this)) + "\n", ENCRYPTION_NONE);
|
||||||
|
m.addPayload(
|
||||||
|
new Element("reply", "urn:xmpp:reply:0")
|
||||||
|
.setAttribute("to", getCounterpart())
|
||||||
|
.setAttribute("id", replyId())
|
||||||
|
);
|
||||||
|
final Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reply:0");
|
||||||
|
fallback.addChild("body", "urn:xmpp:fallback:0")
|
||||||
|
.setAttribute("start", "0")
|
||||||
|
.setAttribute("end", "" + m.body.codePointCount(0, m.body.length()));
|
||||||
|
m.addPayload(fallback);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Element getReply() {
|
||||||
|
if (this.payloads == null) return null;
|
||||||
|
|
||||||
|
for (Element el : this.payloads) {
|
||||||
|
if (el.getName().equals("reply") && el.getNamespace().equals("urn:xmpp:reply:0")) {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public String getConversationUuid() {
|
public String getConversationUuid() {
|
||||||
return conversationUuid;
|
return conversationUuid;
|
||||||
}
|
}
|
||||||
|
@ -351,6 +418,13 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
this.fileParams = null;
|
this.fileParams = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void appendBody(String append) {
|
||||||
|
this.body += append;
|
||||||
|
this.isGeoUri = null;
|
||||||
|
this.isEmojisOnly = null;
|
||||||
|
this.treatAsDownloadable = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void setMucUser(MucOptions.User user) {
|
public void setMucUser(MucOptions.User user) {
|
||||||
this.user = new WeakReference<>(user);
|
this.user = new WeakReference<>(user);
|
||||||
}
|
}
|
||||||
|
@ -775,6 +849,20 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clearPayloads() {
|
||||||
|
this.payloads.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPayload(Element el) {
|
||||||
|
if (el == null) return;
|
||||||
|
|
||||||
|
this.payloads.add(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Element> getPayloads() {
|
||||||
|
return new ArrayList<>(this.payloads);
|
||||||
|
}
|
||||||
|
|
||||||
public void setOob(boolean isOob) {
|
public void setOob(boolean isOob) {
|
||||||
this.oob = isOob;
|
this.oob = isOob;
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,11 @@ public class MessageGenerator extends AbstractGenerator {
|
||||||
if (message.edited()) {
|
if (message.edited()) {
|
||||||
packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat());
|
packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (Element el : message.getPayloads()) {
|
||||||
|
packet.addChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -586,6 +586,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
|
message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
|
||||||
|
|
||||||
|
for (Element el : packet.getChildren()) {
|
||||||
|
if (el.getName().equals("reply") && el.getNamespace() != null && el.getNamespace().equals("urn:xmpp:reply:0")) {
|
||||||
|
message.addPayload(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (conversationMultiMode) {
|
if (conversationMultiMode) {
|
||||||
message.setMucUser(conversation.getMucOptions().findUserByFullJid(counterpart));
|
message.setMucUser(conversation.getMucOptions().findUserByFullJid(counterpart));
|
||||||
final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
|
final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
|
||||||
|
|
|
@ -64,7 +64,7 @@ import eu.siacs.conversations.xmpp.mam.MamReference;
|
||||||
public class DatabaseBackend extends SQLiteOpenHelper {
|
public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
|
|
||||||
private static final String DATABASE_NAME = "history";
|
private static final String DATABASE_NAME = "history";
|
||||||
private static final int DATABASE_VERSION = 51;
|
private static final int DATABASE_VERSION = 52;
|
||||||
|
|
||||||
private static boolean requiresMessageIndexRebuild = false;
|
private static boolean requiresMessageIndexRebuild = false;
|
||||||
private static DatabaseBackend instance = null;
|
private static DatabaseBackend instance = null;
|
||||||
|
@ -601,6 +601,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_MECHANISM + " TEXT");
|
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_MECHANISM + " TEXT");
|
||||||
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_TOKEN + " TEXT");
|
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_TOKEN + " TEXT");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < 52 && newVersion >= 52) {
|
||||||
|
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.PAYLOADS + " TEXT");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void canonicalizeJids(SQLiteDatabase db) {
|
private void canonicalizeJids(SQLiteDatabase db) {
|
||||||
|
|
|
@ -134,6 +134,7 @@ import eu.siacs.conversations.utils.Compatibility;
|
||||||
import eu.siacs.conversations.utils.ConversationsFileObserver;
|
import eu.siacs.conversations.utils.ConversationsFileObserver;
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
import eu.siacs.conversations.utils.EasyOnboardingInvite;
|
import eu.siacs.conversations.utils.EasyOnboardingInvite;
|
||||||
|
import eu.siacs.conversations.utils.Emoticons;
|
||||||
import eu.siacs.conversations.utils.ExceptionHelper;
|
import eu.siacs.conversations.utils.ExceptionHelper;
|
||||||
import eu.siacs.conversations.utils.MimeUtils;
|
import eu.siacs.conversations.utils.MimeUtils;
|
||||||
import eu.siacs.conversations.utils.PhoneHelper;
|
import eu.siacs.conversations.utils.PhoneHelper;
|
||||||
|
@ -564,10 +565,15 @@ public class XmppConnectionService extends Service {
|
||||||
|
|
||||||
public void attachFileToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback<Message> callback) {
|
public void attachFileToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback<Message> callback) {
|
||||||
final Message message;
|
final Message message;
|
||||||
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
|
if (conversation.getReplyTo() == null) {
|
||||||
message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
|
|
||||||
} else {
|
|
||||||
message = new Message(conversation, "", conversation.getNextEncryption());
|
message = new Message(conversation, "", conversation.getNextEncryption());
|
||||||
|
} else {
|
||||||
|
message = conversation.getReplyTo().reply();
|
||||||
|
message.setEncryption(conversation.getNextEncryption());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
|
||||||
|
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||||
}
|
}
|
||||||
if (!Message.configurePrivateFileMessage(message)) {
|
if (!Message.configurePrivateFileMessage(message)) {
|
||||||
message.setCounterpart(conversation.getNextCounterpart());
|
message.setCounterpart(conversation.getNextCounterpart());
|
||||||
|
@ -596,10 +602,14 @@ public class XmppConnectionService extends Service {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Message message;
|
final Message message;
|
||||||
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
|
if (conversation.getReplyTo() == null) {
|
||||||
message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
|
|
||||||
} else {
|
|
||||||
message = new Message(conversation, "", conversation.getNextEncryption());
|
message = new Message(conversation, "", conversation.getNextEncryption());
|
||||||
|
} else {
|
||||||
|
message = conversation.getReplyTo().reply();
|
||||||
|
message.setEncryption(conversation.getNextEncryption());
|
||||||
|
}
|
||||||
|
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
|
||||||
|
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||||
}
|
}
|
||||||
if (!Message.configurePrivateFileMessage(message)) {
|
if (!Message.configurePrivateFileMessage(message)) {
|
||||||
message.setCounterpart(conversation.getNextCounterpart());
|
message.setCounterpart(conversation.getNextCounterpart());
|
||||||
|
@ -973,9 +983,13 @@ public class XmppConnectionService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {
|
private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
|
||||||
final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
|
Message message = new Message(conversation, body, conversation.getNextEncryption());
|
||||||
final Message message = new Message(conversation, body, conversation.getNextEncryption());
|
if (inReplyTo != null) {
|
||||||
|
message = inReplyTo.reply();
|
||||||
|
message.setBody(body);
|
||||||
|
message.setEncryption(conversation.getNextEncryption());
|
||||||
|
}
|
||||||
if (inReplyTo != null && inReplyTo.isPrivateMessage()) {
|
if (inReplyTo != null && inReplyTo.isPrivateMessage()) {
|
||||||
Message.configurePrivateMessage(message, inReplyTo.getCounterpart());
|
Message.configurePrivateMessage(message, inReplyTo.getCounterpart());
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package eu.siacs.conversations.ui;
|
||||||
import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
|
import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
|
||||||
import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION;
|
import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION;
|
||||||
import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard;
|
import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard;
|
||||||
|
import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.showKeyboard;
|
||||||
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
|
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
|
||||||
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
|
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
|
||||||
import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
|
import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
|
||||||
|
@ -25,9 +26,12 @@ import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import android.os.VibrationEffect;
|
||||||
|
import android.os.Vibrator;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.ActionMode;
|
import android.view.ActionMode;
|
||||||
|
@ -123,6 +127,7 @@ import eu.siacs.conversations.ui.util.ViewUtil;
|
||||||
import eu.siacs.conversations.ui.widget.EditMessage;
|
import eu.siacs.conversations.ui.widget.EditMessage;
|
||||||
import eu.siacs.conversations.utils.AccountUtils;
|
import eu.siacs.conversations.utils.AccountUtils;
|
||||||
import eu.siacs.conversations.utils.Compatibility;
|
import eu.siacs.conversations.utils.Compatibility;
|
||||||
|
import eu.siacs.conversations.utils.Emoticons;
|
||||||
import eu.siacs.conversations.utils.GeoHelper;
|
import eu.siacs.conversations.utils.GeoHelper;
|
||||||
import eu.siacs.conversations.utils.MessageUtils;
|
import eu.siacs.conversations.utils.MessageUtils;
|
||||||
import eu.siacs.conversations.utils.NickValidityChecker;
|
import eu.siacs.conversations.utils.NickValidityChecker;
|
||||||
|
@ -194,6 +199,7 @@ public class ConversationFragment extends XmppFragment
|
||||||
private FragmentConversationBinding binding;
|
private FragmentConversationBinding binding;
|
||||||
private Toast messageLoaderToast;
|
private Toast messageLoaderToast;
|
||||||
private ConversationsActivity activity;
|
private ConversationsActivity activity;
|
||||||
|
private Vibrator vibrator;
|
||||||
private boolean reInitRequiredOnStart = true;
|
private boolean reInitRequiredOnStart = true;
|
||||||
|
|
||||||
private ActionMode selectionActionMode;
|
private ActionMode selectionActionMode;
|
||||||
|
@ -840,7 +846,10 @@ public class ConversationFragment extends XmppFragment
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void success(Message message) {
|
public void success(Message message) {
|
||||||
runOnUiThread(() -> activity.hideToast());
|
runOnUiThread(() -> {
|
||||||
|
activity.hideToast();
|
||||||
|
setupReply(null);
|
||||||
|
});
|
||||||
hidePrepareFileToast(prepareFileToast);
|
hidePrepareFileToast(prepareFileToast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -885,6 +894,7 @@ public class ConversationFragment extends XmppFragment
|
||||||
@Override
|
@Override
|
||||||
public void success(Message message) {
|
public void success(Message message) {
|
||||||
hidePrepareFileToast(prepareFileToast);
|
hidePrepareFileToast(prepareFileToast);
|
||||||
|
runOnUiThread(() -> setupReply(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -921,7 +931,14 @@ public class ConversationFragment extends XmppFragment
|
||||||
}
|
}
|
||||||
final Message message;
|
final Message message;
|
||||||
if (conversation.getCorrectingMessage() == null) {
|
if (conversation.getCorrectingMessage() == null) {
|
||||||
message = new Message(conversation, body, conversation.getNextEncryption());
|
if (conversation.getReplyTo() != null) {
|
||||||
|
message = conversation.getReplyTo().reply();
|
||||||
|
message.appendBody(body);
|
||||||
|
message.setEncryption(conversation.getNextEncryption());
|
||||||
|
} else {
|
||||||
|
message = new Message(conversation, body, conversation.getNextEncryption());
|
||||||
|
}
|
||||||
|
|
||||||
Message.configurePrivateMessage(message);
|
Message.configurePrivateMessage(message);
|
||||||
} else {
|
} else {
|
||||||
message = conversation.getCorrectingMessage();
|
message = conversation.getCorrectingMessage();
|
||||||
|
@ -937,6 +954,8 @@ public class ConversationFragment extends XmppFragment
|
||||||
default:
|
default:
|
||||||
sendMessage(message);
|
sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupReply(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) {
|
private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) {
|
||||||
|
@ -1204,6 +1223,7 @@ public class ConversationFragment extends XmppFragment
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"Trying to attach fragment to activity that is not the ConversationsActivity");
|
"Trying to attach fragment to activity that is not the ConversationsActivity");
|
||||||
}
|
}
|
||||||
|
vibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1298,6 +1318,9 @@ public class ConversationFragment extends XmppFragment
|
||||||
binding.textinput.setRichContentListener(new String[] {"image/*"}, mEditorContentListener);
|
binding.textinput.setRichContentListener(new String[] {"image/*"}, mEditorContentListener);
|
||||||
|
|
||||||
binding.textSendButton.setOnClickListener(this.mSendButtonListener);
|
binding.textSendButton.setOnClickListener(this.mSendButtonListener);
|
||||||
|
binding.contextPreviewCancel.setOnClickListener((v) -> {
|
||||||
|
setupReply(null);
|
||||||
|
});
|
||||||
|
|
||||||
binding.scrollToBottomButton.setOnClickListener(this.mScrollButtonListener);
|
binding.scrollToBottomButton.setOnClickListener(this.mScrollButtonListener);
|
||||||
binding.messagesView.setOnScrollListener(mOnScrollListener);
|
binding.messagesView.setOnScrollListener(mOnScrollListener);
|
||||||
|
@ -1334,6 +1357,12 @@ public class ConversationFragment extends XmppFragment
|
||||||
};
|
};
|
||||||
messageListAdapter.setMessageEmptyPartLongClickListener(messageClickListener);
|
messageListAdapter.setMessageEmptyPartLongClickListener(messageClickListener);
|
||||||
messageListAdapter.setSelectionStatusProvider(provider);
|
messageListAdapter.setSelectionStatusProvider(provider);
|
||||||
|
messageListAdapter.setOnMessageBoxSwiped(message -> {
|
||||||
|
if (selectionActionMode == null) {
|
||||||
|
quoteMessage(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
binding.messagesView.setAdapter(messageListAdapter);
|
binding.messagesView.setAdapter(messageListAdapter);
|
||||||
|
|
||||||
registerForContextMenu(binding.messagesView);
|
registerForContextMenu(binding.messagesView);
|
||||||
|
@ -1369,7 +1398,35 @@ public class ConversationFragment extends XmppFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void quoteMessage(Message message) {
|
private void quoteMessage(Message message) {
|
||||||
quoteText(MessageUtils.prepareQuote(message));
|
if (message.isPrivateMessage()) privateMessageWith(message.getCounterpart());
|
||||||
|
setupReply(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupReply(Message message) {
|
||||||
|
Message oldReplyTo = conversation.getReplyTo();
|
||||||
|
conversation.setReplyTo(message);
|
||||||
|
if (message == null) {
|
||||||
|
binding.contextPreview.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldReplyTo == message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SpannableStringBuilder body = message.getBodyForDisplaying();
|
||||||
|
if (message.isFileOrImage() || message.isOOb()) body.append(" 🖼️");
|
||||||
|
messageListAdapter.handleTextQuotes(body, activity.isDarkTheme(), false);
|
||||||
|
binding.contextPreviewText.setText(body);
|
||||||
|
binding.contextPreviewAuthor.setText(message.getAvatarName());
|
||||||
|
binding.contextPreview.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
|
||||||
|
showKeyboard(binding.textinput);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
vibrator.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -2216,6 +2273,7 @@ public class ConversationFragment extends XmppFragment
|
||||||
private void toggleMessageSelection(final Message message) {
|
private void toggleMessageSelection(final Message message) {
|
||||||
if (selectionActionMode == null) {
|
if (selectionActionMode == null) {
|
||||||
activity.startActionMode(actionModeCallback);
|
activity.startActionMode(actionModeCallback);
|
||||||
|
setupReply(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedMessages.contains(message)) {
|
if (selectedMessages.contains(message)) {
|
||||||
|
@ -2560,6 +2618,8 @@ public class ConversationFragment extends XmppFragment
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupReply(conversation.getReplyTo());
|
||||||
|
|
||||||
stopScrolling();
|
stopScrolling();
|
||||||
Log.d(Config.LOGTAG, "reInit(hasExtras=" + hasExtras + ")");
|
Log.d(Config.LOGTAG, "reInit(hasExtras=" + hasExtras + ")");
|
||||||
|
|
||||||
|
|
|
@ -33,11 +33,14 @@ import android.widget.Toast;
|
||||||
import androidx.core.app.ActivityCompat;
|
import androidx.core.app.ActivityCompat;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.cheogram.android.SwipeDetector;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@ -93,9 +96,12 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
|
private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
|
||||||
private MessageEmptyPartClickListener messageEmptyPartClickListener;
|
private MessageEmptyPartClickListener messageEmptyPartClickListener;
|
||||||
private SelectionStatusProvider selectionStatusProvider;
|
private SelectionStatusProvider selectionStatusProvider;
|
||||||
|
private MessageBoxSwipedListener messageBoxSwipedListener;
|
||||||
private boolean mUseGreenBackground = false;
|
private boolean mUseGreenBackground = false;
|
||||||
private final boolean mForceNames;
|
private final boolean mForceNames;
|
||||||
|
|
||||||
|
private Set<SwipeDetector.Action> allowedSwipeActions;
|
||||||
|
|
||||||
|
|
||||||
public MessageAdapter(final XmppActivity activity, final List<Message> messages, final boolean forceNames) {
|
public MessageAdapter(final XmppActivity activity, final List<Message> messages, final boolean forceNames) {
|
||||||
super(activity, 0, messages);
|
super(activity, 0, messages);
|
||||||
|
@ -104,6 +110,8 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
metrics = getContext().getResources().getDisplayMetrics();
|
metrics = getContext().getResources().getDisplayMetrics();
|
||||||
updatePreferences();
|
updatePreferences();
|
||||||
this.mForceNames = forceNames;
|
this.mForceNames = forceNames;
|
||||||
|
allowedSwipeActions = new HashSet<>();
|
||||||
|
allowedSwipeActions.add(SwipeDetector.Action.RL);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageAdapter(final XmppActivity activity, final List<Message> messages) {
|
public MessageAdapter(final XmppActivity activity, final List<Message> messages) {
|
||||||
|
@ -173,6 +181,9 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
this.selectionStatusProvider = provider;
|
this.selectionStatusProvider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOnMessageBoxSwiped(MessageBoxSwipedListener listener) {
|
||||||
|
this.messageBoxSwipedListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getViewTypeCount() {
|
public int getViewTypeCount() {
|
||||||
|
@ -383,27 +394,38 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
viewHolder.messageBody.setText(span);
|
viewHolder.messageBody.setText(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) {
|
private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground, boolean highlightReply) {
|
||||||
if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
|
if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
|
||||||
body.insert(start++, "\n");
|
body.insert(start++, "\n");
|
||||||
body.setSpan(new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
body.setSpan(
|
||||||
|
new DividerSpan(false),
|
||||||
|
start - ("\n".equals(body.subSequence(start - 2, start - 1).toString()) ? 2 : 1),
|
||||||
|
start,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
end++;
|
end++;
|
||||||
}
|
}
|
||||||
if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
|
if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
|
||||||
body.insert(end, "\n");
|
body.insert(end, "\n");
|
||||||
body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
body.setSpan(
|
||||||
|
new DividerSpan(false),
|
||||||
|
end,
|
||||||
|
end + ("\n".equals(body.subSequence(end + 1, end + 2).toString()) ? 2 : 1),
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
}
|
}
|
||||||
int color = darkBackground ? this.getMessageTextColor(darkBackground, false)
|
int color = darkBackground ? this.getMessageTextColor(darkBackground, false)
|
||||||
: ContextCompat.getColor(activity, R.color.green700_desaturated);
|
: ContextCompat.getColor(activity, R.color.green700_desaturated);
|
||||||
|
|
||||||
DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
|
DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
|
||||||
body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
body.setSpan(new QuoteSpan(color, highlightReply ? ContextCompat.getColor(activity, R.color.blue_a100) : -1, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies QuoteSpan to group of lines which starts with > or » characters.
|
* Applies QuoteSpan to group of lines which starts with > or » characters.
|
||||||
* Appends likebreaks and applies DividerSpan to them to show a padding between quote and text.
|
* Appends likebreaks and applies DividerSpan to them to show a padding between quote and text.
|
||||||
*/
|
*/
|
||||||
private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
|
public boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground, boolean highlightReply) {
|
||||||
boolean startsWithQuote = false;
|
boolean startsWithQuote = false;
|
||||||
int quoteDepth = 1;
|
int quoteDepth = 1;
|
||||||
while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
|
while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
|
||||||
|
@ -422,7 +444,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
if (i == 0) startsWithQuote = true;
|
if (i == 0) startsWithQuote = true;
|
||||||
} else if (quoteStart >= 0) {
|
} else if (quoteStart >= 0) {
|
||||||
// Line start without quote, apply spans there
|
// Line start without quote, apply spans there
|
||||||
applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
|
applyQuoteSpan(body, quoteStart, i - 1, darkBackground, quoteDepth == 1 && highlightReply);
|
||||||
quoteStart = -1;
|
quoteStart = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -447,7 +469,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
}
|
}
|
||||||
if (quoteStart >= 0) {
|
if (quoteStart >= 0) {
|
||||||
// Apply spans to finishing open quote
|
// Apply spans to finishing open quote
|
||||||
applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
|
applyQuoteSpan(body, quoteStart, body.length(), darkBackground, quoteDepth == 1 && highlightReply);
|
||||||
}
|
}
|
||||||
quoteDepth++;
|
quoteDepth++;
|
||||||
}
|
}
|
||||||
|
@ -486,7 +508,15 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
int end = body.getSpanEnd(mergeSeparator);
|
int end = body.getSpanEnd(mergeSeparator);
|
||||||
body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
}
|
}
|
||||||
boolean startsWithQuote = handleTextQuotes(body, darkBackground);
|
|
||||||
|
for (final android.text.style.QuoteSpan quote : body.getSpans(0, body.length(), android.text.style.QuoteSpan.class)) {
|
||||||
|
int start = body.getSpanStart(quote);
|
||||||
|
int end = body.getSpanEnd(quote);
|
||||||
|
body.removeSpan(quote);
|
||||||
|
applyQuoteSpan(body, start, end, darkBackground, message.getReply() != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean startsWithQuote = handleTextQuotes(body, darkBackground, message.getReply() != null);
|
||||||
if (!message.isPrivateMessage()) {
|
if (!message.isPrivateMessage()) {
|
||||||
if (hasMeCommand) {
|
if (hasMeCommand) {
|
||||||
body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
|
body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
|
||||||
|
@ -816,6 +846,19 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
viewHolder.clicksInterceptor.setOnClickListener(messageItemClickListener);
|
viewHolder.clicksInterceptor.setOnClickListener(messageItemClickListener);
|
||||||
viewHolder.clicksInterceptor.setOnLongClickListener(messageItemLongClickListener);
|
viewHolder.clicksInterceptor.setOnLongClickListener(messageItemLongClickListener);
|
||||||
|
|
||||||
|
|
||||||
|
SwipeDetector swipeDetector = new SwipeDetector((action) -> {
|
||||||
|
if (action == SwipeDetector.Action.RL && MessageAdapter.this.messageBoxSwipedListener != null) {
|
||||||
|
MessageAdapter.this.messageBoxSwipedListener.onMessageBoxSwiped(message);
|
||||||
|
}
|
||||||
|
}, allowedSwipeActions);
|
||||||
|
|
||||||
|
viewHolder.root.setOnTouchListener(swipeDetector);
|
||||||
|
viewHolder.message_box.setOnTouchListener(swipeDetector);
|
||||||
|
viewHolder.messageBody.setOnTouchListener(swipeDetector);
|
||||||
|
viewHolder.image.setOnTouchListener(swipeDetector);
|
||||||
|
viewHolder.time.setOnTouchListener(swipeDetector);
|
||||||
|
|
||||||
viewHolder.contact_picture.setOnClickListener(v -> {
|
viewHolder.contact_picture.setOnClickListener(v -> {
|
||||||
if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
|
if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
|
||||||
MessageAdapter.this.mOnContactPictureClickedListener
|
MessageAdapter.this.mOnContactPictureClickedListener
|
||||||
|
@ -1051,6 +1094,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
boolean isSomethingSelected();
|
boolean isSomethingSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface MessageBoxSwipedListener {
|
||||||
|
void onMessageBoxSwiped(Message message);
|
||||||
|
}
|
||||||
|
|
||||||
private static class ViewHolder {
|
private static class ViewHolder {
|
||||||
public View root;
|
public View root;
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,8 @@ public class QuoteSpan extends CharacterStyle implements LeadingMarginSpan {
|
||||||
|
|
||||||
private final int color;
|
private final int color;
|
||||||
|
|
||||||
|
private final int dashColor;
|
||||||
|
|
||||||
private final int width;
|
private final int width;
|
||||||
private final int paddingLeft;
|
private final int paddingLeft;
|
||||||
private final int paddingRight;
|
private final int paddingRight;
|
||||||
|
@ -23,8 +25,9 @@ public class QuoteSpan extends CharacterStyle implements LeadingMarginSpan {
|
||||||
private static final float PADDING_LEFT_SP = 1.5f;
|
private static final float PADDING_LEFT_SP = 1.5f;
|
||||||
private static final float PADDING_RIGHT_SP = 8f;
|
private static final float PADDING_RIGHT_SP = 8f;
|
||||||
|
|
||||||
public QuoteSpan(int color, DisplayMetrics metrics) {
|
public QuoteSpan(int color, int dashColor, DisplayMetrics metrics) {
|
||||||
this.color = color;
|
this.color = color;
|
||||||
|
this.dashColor = dashColor;
|
||||||
this.width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, WIDTH_SP, metrics);
|
this.width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, WIDTH_SP, metrics);
|
||||||
this.paddingLeft = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_LEFT_SP, metrics);
|
this.paddingLeft = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_LEFT_SP, metrics);
|
||||||
this.paddingRight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_RIGHT_SP, metrics);
|
this.paddingRight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_RIGHT_SP, metrics);
|
||||||
|
@ -46,7 +49,11 @@ public class QuoteSpan extends CharacterStyle implements LeadingMarginSpan {
|
||||||
Paint.Style style = p.getStyle();
|
Paint.Style style = p.getStyle();
|
||||||
int color = p.getColor();
|
int color = p.getColor();
|
||||||
p.setStyle(Paint.Style.FILL);
|
p.setStyle(Paint.Style.FILL);
|
||||||
p.setColor(this.color);
|
if (dashColor != -1) {
|
||||||
|
p.setColor(this.dashColor);
|
||||||
|
} else {
|
||||||
|
p.setColor(this.color);
|
||||||
|
}
|
||||||
c.drawRect(x + dir * paddingLeft, top, x + dir * (paddingLeft + width), bottom, p);
|
c.drawRect(x + dir * paddingLeft, top, x + dir * (paddingLeft + width), bottom, p);
|
||||||
p.setStyle(style);
|
p.setStyle(style);
|
||||||
p.setColor(color);
|
p.setColor(color);
|
||||||
|
|
|
@ -103,4 +103,15 @@ public class QuoteHelper {
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public static String quote(String text) {
|
||||||
|
text = replaceAltQuoteCharsInText(text);
|
||||||
|
return text
|
||||||
|
// first replace all '>' at the beginning of the line with nice and tidy '>>'
|
||||||
|
// for nested quoting
|
||||||
|
.replaceAll("(^|\n)(" + QUOTE_CHAR + ")", "$1$2$2")
|
||||||
|
// then find all other lines and have them start with a '> '
|
||||||
|
.replaceAll("(^|\n)(?!" + QUOTE_CHAR + ")(.*)", "$1> $2")
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
6
src/main/java/eu/siacs/conversations/utils/Consumer.java
Normal file
6
src/main/java/eu/siacs/conversations/utils/Consumer.java
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package eu.siacs.conversations.utils;
|
||||||
|
|
||||||
|
// Based on java.util.function.Consumer to avoid Android 24 dependency
|
||||||
|
public interface Consumer<T> {
|
||||||
|
void accept(T t);
|
||||||
|
}
|
|
@ -20,7 +20,60 @@
|
||||||
android:listSelector="@android:color/transparent"
|
android:listSelector="@android:color/transparent"
|
||||||
android:stackFromBottom="true"
|
android:stackFromBottom="true"
|
||||||
android:transcriptMode="normal"
|
android:transcriptMode="normal"
|
||||||
tools:listitem="@layout/message_sent"></ListView>
|
tools:listitem="@layout/message_sent"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/context_preview"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_above="@+id/textsend"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="40dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingLeft="8dp"
|
||||||
|
android:paddingRight="14dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:background="?attr/color_background_primary">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:src="?attr/icon_quote"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:contentDescription="Reply to" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/context_preview_author"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="sans-serif-medium" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/context_preview_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/context_preview_cancel"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:contentDescription="Cancel"
|
||||||
|
android:background="?attr/color_background_primary"
|
||||||
|
android:src="?attr/icon_cancel" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/scroll_to_bottom_button"
|
android:id="@+id/scroll_to_bottom_button"
|
||||||
|
@ -128,7 +181,7 @@
|
||||||
android:id="@+id/snackbar"
|
android:id="@+id/snackbar"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_above="@+id/textsend"
|
android:layout_above="@+id/context_preview"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_marginRight="8dp"
|
android:layout_marginRight="8dp"
|
||||||
android:layout_marginBottom="4dp"
|
android:layout_marginBottom="4dp"
|
||||||
|
|
Loading…
Reference in a new issue