1
0
Fork 1

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:
Arne 2023-05-05 23:49:54 +02:00
parent adb078c9a8
commit 1c0a9f287e
14 changed files with 506 additions and 24 deletions

View file

@ -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 {

View 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);
}
}
}

View 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);
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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 { }
}

View file

@ -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

View file

@ -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);
}

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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"

View file

@ -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>