quote as extra line, quote improvements for slidge, only set "note to self" on *copy*, reduce RAM pressure by the cache a bit (Cheogram)
This commit is contained in:
parent
adb078c9a8
commit
1c0a9f287e
14 changed files with 506 additions and 24 deletions
|
@ -96,8 +96,10 @@ dependencies {
|
|||
implementation 'com.github.AppIntro:AppIntro:6.2.0'
|
||||
implementation 'androidx.browser:browser:1.4.0'
|
||||
implementation 'com.otaliastudios:transcoder:0.9.1' // 0.10.4 seems to be buggy
|
||||
implementation 'me.saket:better-link-movement-method:2.2.0'
|
||||
implementation 'com.github.singpolyma:Better-Link-Movement-Method:4df081e1e4'
|
||||
implementation project(':libs:AXML')
|
||||
implementation 'com.github.ipld:java-cid:v1.3.1'
|
||||
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
199
src/main/java/de/monocles/chat/BobTransfer.java
Normal file
199
src/main/java/de/monocles/chat/BobTransfer.java
Normal file
|
@ -0,0 +1,199 @@
|
|||
package de.monocles.chat;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import io.ipfs.cid.Cid;
|
||||
import io.ipfs.multihash.Multihash;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Transferable;
|
||||
import eu.siacs.conversations.http.AesGcmURL;
|
||||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.MimeUtils;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
|
||||
public class BobTransfer implements Transferable {
|
||||
protected int status = Transferable.STATUS_OFFER;
|
||||
protected URI uri;
|
||||
protected Account account;
|
||||
protected Jid to;
|
||||
protected XmppConnectionService xmppConnectionService;
|
||||
protected static Map<URI, Long> attempts = new HashMap<>();
|
||||
|
||||
public static Cid cid(Uri uri) {
|
||||
if (uri == null || uri.getScheme() == null || !uri.getScheme().equals("cid")) return null;
|
||||
return cid(uri.getSchemeSpecificPart());
|
||||
}
|
||||
|
||||
public static Cid cid(URI uri) {
|
||||
if (uri == null || uri.getScheme() == null || !uri.getScheme().equals("cid")) return null;
|
||||
return cid(uri.getSchemeSpecificPart());
|
||||
}
|
||||
|
||||
public static Cid cid(String bobCid) {
|
||||
if (!bobCid.contains("@") || !bobCid.contains("+")) return null;
|
||||
String[] cidParts = bobCid.split("@")[0].split("\\+");
|
||||
try {
|
||||
return CryptoHelper.cid(CryptoHelper.hexToBytes(cidParts[1]), cidParts[0]);
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static URI uri(Cid cid) throws NoSuchAlgorithmException, URISyntaxException {
|
||||
return new URI("cid", multihashAlgo(cid.getType()) + "+" + CryptoHelper.bytesToHex(cid.getHash()) + "@bob.xmpp.org", null);
|
||||
}
|
||||
|
||||
private static String multihashAlgo(Multihash.Type type) throws NoSuchAlgorithmException {
|
||||
final String algo = CryptoHelper.multihashAlgo(type);
|
||||
if (algo.equals("sha-1")) return "sha1";
|
||||
return algo;
|
||||
}
|
||||
|
||||
public BobTransfer(URI uri, Account account, Jid to, XmppConnectionService xmppConnectionService) {
|
||||
this.xmppConnectionService = xmppConnectionService;
|
||||
this.uri = uri;
|
||||
this.to = to;
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean start() {
|
||||
if (status == Transferable.STATUS_DOWNLOADING) return true;
|
||||
File f = xmppConnectionService.getFileForCid(cid(uri));
|
||||
|
||||
if (f != null && f.canRead()) {
|
||||
finish(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (xmppConnectionService.hasInternetConnection() && attempts.getOrDefault(uri, 0L) + 10000L < System.currentTimeMillis()) {
|
||||
attempts.put(uri, System.currentTimeMillis());
|
||||
changeStatus(Transferable.STATUS_DOWNLOADING);
|
||||
|
||||
IqPacket request = new IqPacket(IqPacket.TYPE.GET);
|
||||
request.setTo(to);
|
||||
final Element dataq = request.addChild("data", "urn:xmpp:bob");
|
||||
dataq.setAttribute("cid", uri.getSchemeSpecificPart());
|
||||
xmppConnectionService.sendIqPacket(account, request, (acct, packet) -> {
|
||||
final Element data = packet.findChild("data", "urn:xmpp:bob");
|
||||
if (packet.getType() == IqPacket.TYPE.ERROR || data == null) {
|
||||
Log.d(Config.LOGTAG, "BobTransfer failed: " + packet);
|
||||
finish(null);
|
||||
} else {
|
||||
final String contentType = data.getAttribute("type");
|
||||
String fileExtension = "dat";
|
||||
if (contentType != null) {
|
||||
fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
|
||||
}
|
||||
|
||||
try {
|
||||
final byte[] bytes = Base64.decode(data.getContent(), Base64.DEFAULT);
|
||||
|
||||
File file = xmppConnectionService.getFileBackend().getStorageLocation(new ByteArrayInputStream(bytes), fileExtension);
|
||||
file.getParentFile().mkdirs();
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
throw new IOException(file.getAbsolutePath());
|
||||
}
|
||||
|
||||
final OutputStream outputStream = AbstractConnectionManager.createOutputStream(new DownloadableFile(file.getAbsolutePath()), false, false);
|
||||
|
||||
if (outputStream != null && bytes != null) {
|
||||
outputStream.write(bytes);
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
finish(file);
|
||||
} else {
|
||||
Log.w(Config.LOGTAG, "Could not write BobTransfer, null outputStream");
|
||||
finish(null);
|
||||
}
|
||||
} catch (final IOException | XmppConnectionService.BlockedMediaException e) {
|
||||
Log.w(Config.LOGTAG, "Could not write BobTransfer: " + e);
|
||||
finish(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProgress() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getFileSize() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
// No real way to cancel an iq in process...
|
||||
changeStatus(Transferable.STATUS_CANCELLED);
|
||||
}
|
||||
|
||||
protected void changeStatus(int newStatus) {
|
||||
status = newStatus;
|
||||
xmppConnectionService.updateConversationUi();
|
||||
}
|
||||
|
||||
protected void finish(File f) {
|
||||
if (f != null) xmppConnectionService.updateConversationUi();
|
||||
}
|
||||
|
||||
public static class ForMessage extends BobTransfer {
|
||||
protected Message message;
|
||||
|
||||
public ForMessage(Message message, XmppConnectionService xmppConnectionService) throws URISyntaxException {
|
||||
super(new URI(message.getFileParams().url), message.getConversation().getAccount(), message.getCounterpart(), xmppConnectionService);
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
super.cancel();
|
||||
message.setTransferable(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finish(File f) {
|
||||
if (f != null) {
|
||||
message.setRelativeFilePath(f.getAbsolutePath());
|
||||
final boolean privateMessage = message.isPrivateMessage();
|
||||
message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
|
||||
xmppConnectionService.getFileBackend().updateFileParams(message, uri.toString(), false);
|
||||
xmppConnectionService.updateMessage(message);
|
||||
}
|
||||
message.setTransferable(null);
|
||||
super.finish(f);
|
||||
}
|
||||
}
|
||||
}
|
9
src/main/java/de/monocles/chat/GetThumbnailForCid.java
Normal file
9
src/main/java/de/monocles/chat/GetThumbnailForCid.java
Normal file
|
@ -0,0 +1,9 @@
|
|||
package de.monocles.chat;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import io.ipfs.cid.Cid;
|
||||
|
||||
public interface GetThumbnailForCid {
|
||||
public Drawable getThumbnail(Cid cid);
|
||||
}
|
|
@ -72,6 +72,10 @@ public class Contact implements ListItem, Blockable {
|
|||
private long mLastseen = 0;
|
||||
private String mLastPresence = null;
|
||||
private RtpCapability.Capability rtpCapability;
|
||||
public Contact(Contact other) {
|
||||
this(other.getAccount().getUuid(), other.systemName, other.serverName, other.presenceName, other.jid, other.subscription, other.photoUri, other.systemAccount, other.keys.toString(), other.getAvatar().sha1sum, other.mLastseen, other.mLastPresence, other.groups.toString(), other.rtpCapability);
|
||||
setAccount(other.getAccount());
|
||||
}
|
||||
|
||||
public Contact(final String account, final String systemName, final String serverName, final String presenceName,
|
||||
final Jid jid, final int subscription, final String photoUri,
|
||||
|
|
|
@ -4,9 +4,16 @@ import android.annotation.SuppressLint;
|
|||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
|
||||
import eu.siacs.conversations.ui.util.QuoteHelper;
|
||||
|
||||
|
@ -14,6 +21,8 @@ import com.google.common.base.Strings;
|
|||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.io.ByteSource;
|
||||
import com.google.common.primitives.Longs;
|
||||
import de.monocles.chat.BobTransfer;
|
||||
import de.monocles.chat.GetThumbnailForCid;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
@ -37,7 +46,6 @@ import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
|||
import eu.siacs.conversations.http.URL;
|
||||
import eu.siacs.conversations.services.AvatarService;
|
||||
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.Emoticons;
|
||||
import eu.siacs.conversations.utils.GeoHelper;
|
||||
|
@ -51,6 +59,7 @@ import eu.siacs.conversations.xml.Namespace;
|
|||
import eu.siacs.conversations.xml.Tag;
|
||||
import eu.siacs.conversations.xml.XmlReader;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import io.ipfs.cid.Cid;
|
||||
|
||||
public class Message extends AbstractEntity implements AvatarService.Avatarable {
|
||||
|
||||
|
@ -375,10 +384,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
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.length());
|
||||
.setAttribute("end", "" + m.body.codePointCount(0, m.body.length()));
|
||||
m.addPayload(fallback);
|
||||
return m;
|
||||
}
|
||||
|
||||
public Message react(String emoji) {
|
||||
Set<String> emojis = new HashSet<>();
|
||||
if (conversation instanceof Conversation) emojis = ((Conversation) conversation).findReactionsTo(replyId(), null);
|
||||
|
@ -918,9 +928,68 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
|
||||
public static class MergeSeparator {
|
||||
}
|
||||
public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
|
||||
final Element html = getHtml();
|
||||
if (html == null || Build.VERSION.SDK_INT < 24) {
|
||||
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim());
|
||||
} else {
|
||||
SpannableStringBuilder spannable = new SpannableStringBuilder(Html.fromHtml(
|
||||
MessageUtils.filterLtrRtl(html.toString()).trim(),
|
||||
Html.FROM_HTML_MODE_COMPACT,
|
||||
(source) -> {
|
||||
try {
|
||||
if (thumbnailer == null) return fallbackImg;
|
||||
Cid cid = BobTransfer.cid(new URI(source));
|
||||
if (cid == null) return fallbackImg;
|
||||
Drawable thumbnail = thumbnailer.getThumbnail(cid);
|
||||
if (thumbnail == null) return fallbackImg;
|
||||
return thumbnail;
|
||||
} catch (final URISyntaxException e) {
|
||||
return fallbackImg;
|
||||
}
|
||||
},
|
||||
(opening, tag, output, xmlReader) -> {}
|
||||
));
|
||||
|
||||
// Make images clickable and long-clickable with BetterLinkMovementMethod
|
||||
ImageSpan[] imageSpans = spannable.getSpans(0, spannable.length(), ImageSpan.class);
|
||||
for (ImageSpan span : imageSpans) {
|
||||
final int start = spannable.getSpanStart(span);
|
||||
final int end = spannable.getSpanEnd(span);
|
||||
|
||||
ClickableSpan click_span = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View widget) { }
|
||||
};
|
||||
|
||||
spannable.setSpan(click_span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/10187511/8611
|
||||
int i = spannable.length();
|
||||
while(--i >= 0 && Character.isWhitespace(spannable.charAt(i))) { }
|
||||
return (SpannableStringBuilder) spannable.subSequence(0, i+1);
|
||||
}
|
||||
}
|
||||
|
||||
public Element getHtml() {
|
||||
if (this.payloads == null) return null;
|
||||
|
||||
for (Element el : this.payloads) {
|
||||
if (el.getName().equals("html") && el.getNamespace().equals("http://jabber.org/protocol/xhtml-im")) {
|
||||
return el.getChildren().get(0);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public SpannableStringBuilder getMergedBody() {
|
||||
SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
|
||||
return getMergedBody(null, null);
|
||||
}
|
||||
|
||||
public SpannableStringBuilder getMergedBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
|
||||
SpannableStringBuilder body = getSpannableBody(thumbnailer, fallbackImg);
|
||||
Message current = this;
|
||||
while (current.mergeable(current.next())) {
|
||||
current = current.next();
|
||||
|
@ -928,8 +997,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
break;
|
||||
}
|
||||
body.append("\n\n");
|
||||
body.setSpan(new MergeSeparator(), body.length() - 2, body.length(), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
|
||||
body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
|
||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
body.append(current.getSpannableBody(thumbnailer, fallbackImg));
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
|
|||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.PresenceTemplate;
|
||||
import eu.siacs.conversations.entities.Roster;
|
||||
|
@ -63,6 +64,7 @@ import eu.siacs.conversations.utils.Resolver;
|
|||
import eu.siacs.conversations.xmpp.InvalidJid;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.mam.MamReference;
|
||||
import io.ipfs.cid.Cid;
|
||||
|
||||
public class DatabaseBackend extends SQLiteOpenHelper {
|
||||
|
||||
|
@ -1051,6 +1053,43 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
return filesPaths;
|
||||
}
|
||||
|
||||
public DownloadableFile getFileForCid(Cid cid) {
|
||||
SQLiteDatabase db = this.getReadableDatabase();
|
||||
Cursor cursor = db.query("cheogram.cids", new String[]{"path"}, "cid=?", new String[]{cid.toString()}, null, null, null);
|
||||
DownloadableFile f = null;
|
||||
if (cursor.moveToNext()) {
|
||||
f = new DownloadableFile(cursor.getString(0));
|
||||
}
|
||||
cursor.close();
|
||||
return f;
|
||||
}
|
||||
|
||||
public boolean isBlockedMedia(Cid cid) {
|
||||
SQLiteDatabase db = this.getReadableDatabase();
|
||||
Cursor cursor = db.query("cheogram.blocked_media", new String[]{"count(*)"}, "cid=?", new String[]{cid.toString()}, null, null, null);
|
||||
boolean is = false;
|
||||
if (cursor.moveToNext()) {
|
||||
is = cursor.getInt(0) > 0;
|
||||
}
|
||||
cursor.close();
|
||||
return is;
|
||||
}
|
||||
|
||||
public void saveCid(Cid cid, File file) {
|
||||
saveCid(cid, file, null);
|
||||
}
|
||||
|
||||
public void saveCid(Cid cid, File file, String url) {
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put("cid", cid.toString());
|
||||
if (file != null) cv.put("path", file.getAbsolutePath());
|
||||
if (url != null) cv.put("url", url);
|
||||
if (db.update("monocles.cids", cv, "cid=?", new String[]{cid.toString()}) < 1) {
|
||||
db.insertWithOnConflict("monocles.cids", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
}
|
||||
|
||||
public static class FilePath {
|
||||
public final UUID uuid;
|
||||
public final String path;
|
||||
|
|
|
@ -50,6 +50,7 @@ import androidx.exifinterface.media.ExifInterface;
|
|||
import com.google.common.base.Strings;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
|
@ -98,6 +99,7 @@ import eu.siacs.conversations.utils.MimeUtils;
|
|||
import eu.siacs.conversations.xmpp.pep.Avatar;
|
||||
import ezvcard.Ezvcard;
|
||||
import ezvcard.VCard;
|
||||
import io.ipfs.cid.Cid;
|
||||
import me.drakeet.support.toast.ToastCompat;
|
||||
|
||||
public class FileBackend {
|
||||
|
@ -1594,8 +1596,10 @@ public class FileBackend {
|
|||
public void updateFileParams(Message message) {
|
||||
updateFileParams(message, null);
|
||||
}
|
||||
|
||||
public void updateFileParams(final Message message, final String url) {
|
||||
updateFileParams(message, url, true);
|
||||
}
|
||||
public void updateFileParams(final Message message, String url, boolean updateCids) {
|
||||
final boolean encrypted =
|
||||
message.getEncryption() == Message.ENCRYPTION_PGP
|
||||
|| message.getEncryption() == Message.ENCRYPTION_DECRYPTED;
|
||||
|
@ -1953,6 +1957,27 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
|
||||
public File getStorageLocation(final InputStream is, final String extension) throws IOException, XmppConnectionService.BlockedMediaException {
|
||||
final String mime = MimeUtils.guessMimeTypeFromExtension(extension);
|
||||
Cid[] cids = calculateCids(is);
|
||||
|
||||
File file = getStorageLocation(String.format("%s.%s", cids[0], extension), mime);
|
||||
for (int i = 0; i < cids.length; i++) {
|
||||
mXmppConnectionService.saveCid(cids[i], file);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
public Cid[] calculateCids(final Uri uri) throws IOException {
|
||||
return calculateCids(mXmppConnectionService.getContentResolver().openInputStream(uri));
|
||||
}
|
||||
|
||||
public Cid[] calculateCids(final InputStream is) throws IOException {
|
||||
try {
|
||||
return CryptoHelper.cid(is, new String[]{"SHA-256", "SHA-1", "SHA-512"});
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
private static class Dimensions {
|
||||
public final int width;
|
||||
public final int height;
|
||||
|
|
|
@ -71,6 +71,7 @@ import net.java.otr4j.session.SessionID;
|
|||
import net.java.otr4j.session.SessionImpl;
|
||||
import net.java.otr4j.session.SessionStatus;
|
||||
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.utils.ThemeHelper;
|
||||
import eu.siacs.conversations.xmpp.jid.OtrJidHelper;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
@ -210,6 +211,7 @@ import eu.siacs.conversations.xmpp.pep.PublishOptions;
|
|||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
|
||||
import io.ipfs.cid.Cid;
|
||||
import me.leolin.shortcutbadger.ShortcutBadger;
|
||||
|
||||
public class XmppConnectionService extends Service {
|
||||
|
@ -1453,7 +1455,7 @@ public class XmppConnectionService extends Service {
|
|||
Resolver.init(this);
|
||||
this.mRandom = new SecureRandom();
|
||||
updateMemorizingTrustmanager();
|
||||
final int DEFAULT_CACHE_SIZE_PROPORTION = 8;
|
||||
final int DEFAULT_CACHE_SIZE_PROPORTION = 10;
|
||||
ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
int memoryClass = manager.getMemoryClass();
|
||||
int memoryClassInKilobytes = memoryClass * 1024;
|
||||
|
@ -5623,6 +5625,21 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
public DownloadableFile getFileForCid(Cid cid) {
|
||||
return this.databaseBackend.getFileForCid(cid);
|
||||
}
|
||||
|
||||
public void saveCid(Cid cid, File file) throws BlockedMediaException {
|
||||
saveCid(cid, file, null);
|
||||
}
|
||||
|
||||
public void saveCid(Cid cid, File file, String url) throws BlockedMediaException {
|
||||
if (this.databaseBackend.isBlockedMedia(cid)) {
|
||||
throw new BlockedMediaException();
|
||||
}
|
||||
this.databaseBackend.saveCid(cid, file, url);
|
||||
}
|
||||
|
||||
public interface OnMamPreferencesFetched {
|
||||
void onPreferencesFetched(Element prefs);
|
||||
|
||||
|
@ -5779,4 +5796,5 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
}).start();
|
||||
}
|
||||
public static class BlockedMediaException extends Exception { }
|
||||
}
|
|
@ -82,6 +82,7 @@ import androidx.core.content.ContextCompat;
|
|||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
|
||||
|
@ -101,6 +102,7 @@ import java.util.Objects;
|
|||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import eu.siacs.conversations.utils.TimeFrameUtils;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
|
@ -972,7 +974,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
return;
|
||||
}
|
||||
final Editable text = this.binding.textinput.getText();
|
||||
final String body = text == null ? "" : text.toString();
|
||||
String body = text == null ? "" : text.toString();
|
||||
final Conversation conversation = this.conversation;
|
||||
if (body.length() == 0 || conversation == null) {
|
||||
return;
|
||||
|
@ -982,6 +984,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
}
|
||||
final Message message;
|
||||
if (conversation.getCorrectingMessage() == null) {
|
||||
boolean attention = false;
|
||||
if (Pattern.compile("\\A@here\\s.*").matcher(body).find()) {
|
||||
attention = true;
|
||||
body = body.replaceFirst("\\A@here\\s+", "");
|
||||
}
|
||||
if (conversation.getReplyTo() != null) {
|
||||
if (Emoticons.isEmoji(body)) {
|
||||
message = conversation.getReplyTo().react(body);
|
||||
|
@ -994,11 +1001,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
message = new Message(conversation, body, conversation.getNextEncryption());
|
||||
}
|
||||
message.setThread(conversation.getThread());
|
||||
if (attention) {
|
||||
message.addPayload(new Element("attention", "urn:xmpp:attention:0"));
|
||||
}
|
||||
Message.configurePrivateMessage(message);
|
||||
} else {
|
||||
message = conversation.getCorrectingMessage();
|
||||
message.putEdited(message.getUuid(), message.getServerMsgId(), message.getBody(), message.getTimeSent());
|
||||
message.setBody(body);
|
||||
message.setThread(conversation.getThread());
|
||||
message.putEdited(message.getUuid(), message.getServerMsgId(), message.getBody(), message.getTimeSent());
|
||||
message.setServerMsgId(null);
|
||||
message.setUuid(UUID.randomUUID().toString());
|
||||
}
|
||||
|
@ -1012,6 +1023,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
default:
|
||||
sendMessage(message);
|
||||
}
|
||||
setupReply(null);
|
||||
}
|
||||
|
||||
private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) {
|
||||
|
@ -1353,6 +1365,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
binding.textinput.setRichContentListener(new String[]{"image/*"}, mEditorContentListener);
|
||||
|
||||
binding.textSendButton.setOnClickListener(this.mSendButtonListener);
|
||||
binding.contextPreviewCancel.setOnClickListener((v) -> {
|
||||
setupReply(null);
|
||||
});
|
||||
binding.textSendButton.setOnLongClickListener(this.mSendButtonLongListener);
|
||||
binding.scrollToBottomButton.setOnClickListener(this.mScrollButtonListener);
|
||||
binding.recordVoiceButton.setOnClickListener(this.mRecordVoiceButtonListener);
|
||||
|
@ -1447,18 +1462,23 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
|||
private void quoteMessage(Message message, @Nullable String user) {
|
||||
if (message.isGeoUri()) {
|
||||
quoteGeoUri(message, user);
|
||||
} else if (message.isFileOrImage()) {
|
||||
quoteMedia(message, user);
|
||||
} else if (message.isTypeText()) {
|
||||
final StringBuilder stringBuilder = new StringBuilder();
|
||||
if (activity.showDateInQuotes()) {
|
||||
stringBuilder.append(df.format(message.getTimeSent())).append(System.getProperty("line.separator"));
|
||||
}
|
||||
stringBuilder.append(MessageUtils.prepareQuote(message));
|
||||
quoteText(stringBuilder.toString(), user);
|
||||
}
|
||||
setupReply(message);
|
||||
}
|
||||
|
||||
private void setupReply(Message message) {
|
||||
conversation.setReplyTo(message);
|
||||
if (message == null) {
|
||||
binding.contextPreview.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
SpannableStringBuilder body = message.getSpannableBody(null, null);
|
||||
if (message.isFileOrImage() || message.isOOb()) body.append(" 🖼️");
|
||||
messageListAdapter.handleTextQuotes(body, activity.isDarkTheme());
|
||||
binding.contextPreviewText.setText(body);
|
||||
binding.contextPreview.setVisibility(View.VISIBLE);
|
||||
}
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
//This should cancel any remaining click events that would otherwise trigger links
|
||||
|
|
|
@ -997,9 +997,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
|
|||
this.contacts.add(contact);
|
||||
}
|
||||
}
|
||||
final Contact self = account.getSelfContact();
|
||||
final Contact self = new Contact(account.getSelfContact());
|
||||
self.setSystemName("Note to Self");
|
||||
if (self.match(this, needle)) {
|
||||
self.setSystemName(getString(R.string.note_to_self));
|
||||
this.contacts.add(self);
|
||||
}
|
||||
|
||||
|
|
|
@ -576,7 +576,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
* 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.
|
||||
*/
|
||||
private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
|
||||
public boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
|
||||
boolean startsWithQuote = false;
|
||||
int quoteDepth = 1;
|
||||
while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
|
||||
|
|
|
@ -10,6 +10,8 @@ import org.bouncycastle.asn1.x500.style.BCStyle;
|
|||
import org.bouncycastle.asn1.x500.style.IETFUtils;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
@ -30,6 +32,8 @@ import eu.siacs.conversations.R;
|
|||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import io.ipfs.cid.Cid;
|
||||
import io.ipfs.multihash.Multihash;
|
||||
|
||||
public final class CryptoHelper {
|
||||
|
||||
|
@ -283,4 +287,55 @@ public final class CryptoHelper {
|
|||
final String u = url.toLowerCase();
|
||||
return !u.contains(" ") && (u.startsWith("https://") || u.startsWith("http://") || u.startsWith("p1s3://")) && u.endsWith(".pgp");
|
||||
}
|
||||
|
||||
|
||||
public static String multihashAlgo(Multihash.Type type) throws NoSuchAlgorithmException {
|
||||
switch(type) {
|
||||
case sha1:
|
||||
return "sha-1";
|
||||
case sha2_256:
|
||||
return "sha-256";
|
||||
case sha2_512:
|
||||
return "sha-512";
|
||||
default:
|
||||
throw new NoSuchAlgorithmException("" + type);
|
||||
}
|
||||
}
|
||||
|
||||
public static Multihash.Type multihashType(String algo) throws NoSuchAlgorithmException {
|
||||
if (algo.equals("SHA-1") || algo.equals("sha-1") || algo.equals("sha1")) {
|
||||
return Multihash.Type.sha1;
|
||||
} else if (algo.equals("SHA-256") || algo.equals("sha-256")) {
|
||||
return Multihash.Type.sha2_256;
|
||||
} else if (algo.equals("SHA-512") | algo.equals("sha-512")) {
|
||||
return Multihash.Type.sha2_512;
|
||||
} else {
|
||||
throw new NoSuchAlgorithmException(algo);
|
||||
}
|
||||
}
|
||||
|
||||
public static Cid cid(byte[] digest, String algo) throws NoSuchAlgorithmException {
|
||||
return Cid.buildCidV1(Cid.Codec.Raw, multihashType(algo), digest);
|
||||
}
|
||||
|
||||
public static Cid[] cid(InputStream in, String[] algo) throws NoSuchAlgorithmException, IOException {
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
MessageDigest[] md = new MessageDigest[algo.length];
|
||||
for (int i = 0; i < md.length; i++) {
|
||||
md[i] = MessageDigest.getInstance(algo[i]);
|
||||
}
|
||||
while ((len = in.read(buf)) != -1) {
|
||||
for (int i = 0; i < md.length; i++) {
|
||||
md[i].update(buf, 0, len);
|
||||
}
|
||||
}
|
||||
|
||||
Cid[] cid = new Cid[md.length];
|
||||
for (int i = 0; i < cid.length; i++) {
|
||||
cid[i] = cid(md[i].digest(), algo[i]);
|
||||
}
|
||||
|
||||
return cid;
|
||||
}
|
||||
}
|
|
@ -53,6 +53,44 @@
|
|||
android:visibility="gone"
|
||||
app:backgroundColor="?attr/colorAccent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/context_preview"
|
||||
android:visibility="gone"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_above="@+id/input"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
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="@string/reply_to" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/context_preview_text"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/context_preview_cancel"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:padding="0dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:contentDescription="Cancel"
|
||||
android:background="?attr/color_background_primary"
|
||||
android:src="?attr/icon_cancel" />
|
||||
</LinearLayout>
|
||||
<RelativeLayout
|
||||
android:id="@+id/input"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -63,7 +101,7 @@
|
|||
|
||||
<RelativeLayout
|
||||
android:id="@+id/textsend"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
|
@ -71,7 +109,9 @@
|
|||
android:paddingStart="2dp"
|
||||
android:paddingLeft="2dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp">
|
||||
android:paddingBottom="2dp"
|
||||
android:background="?attr/color_background_primary">
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/recordVoiceButton"
|
||||
|
|
|
@ -1260,4 +1260,5 @@
|
|||
<string name="outgoing_call_timestamp">Outgoing call · %s</string>
|
||||
<string name="note_to_self">Note to self</string>
|
||||
<string name="retract_message">Retract message</string>
|
||||
<string name="reply_to">Reply to</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue