Detailed changes
@@ -47,6 +47,7 @@ import eu.siacs.conversations.crypto.axolotl.AxolotlService;
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.MyLinkify;
import eu.siacs.conversations.ui.util.PresenceSelector;
import eu.siacs.conversations.ui.util.QuoteHelper;
import eu.siacs.conversations.utils.CryptoHelper;
@@ -788,7 +789,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return false;
} else {
String body, otherBody;
- if (this.hasFileOnRemoteHost()) {
+ if (this.hasFileOnRemoteHost() && (this.body == null || "".equals(this.body))) {
body = getFileParams().url;
otherBody = message.body == null ? null : message.body.trim();
} else {
@@ -1081,6 +1082,16 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
}
+ public List<URI> getLinks() {
+ return MyLinkify.extractLinks(new SpannableStringBuilder(getBody())).stream().map((url) -> {
+ try {
+ return new URI(url);
+ } catch (final URISyntaxException e) {
+ return null;
+ }
+ }).filter(x -> x != null).collect(Collectors.toList());
+ }
+
public URI getOob() {
final String url = getFileParams().url;
try {
@@ -106,17 +106,19 @@ public class MessageGenerator extends AbstractGenerator {
if (message.hasFileOnRemoteHost()) {
final Message.FileParams fileParams = message.getFileParams();
- if (message.getBody().equals("")) {
- message.setBody(fileParams.url);
- packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB)
- .addChild("body", "urn:xmpp:fallback:0");
- } else {
- long start = message.getQuoteableBody().length();
- message.appendBody(fileParams.url);
- packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB)
- .addChild("body", "urn:xmpp:fallback:0")
- .setAttribute("start", String.valueOf(start))
- .setAttribute("end", String.valueOf(start + fileParams.url.length()));
+ if (message.getFallbacks(Namespace.OOB).isEmpty()) {
+ if (message.getBody().equals("")) {
+ message.setBody(fileParams.url);
+ packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB)
+ .addChild("body", "urn:xmpp:fallback:0");
+ } else {
+ long start = message.getQuoteableBody().length();
+ message.appendBody(fileParams.url);
+ packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB)
+ .addChild("body", "urn:xmpp:fallback:0")
+ .setAttribute("start", String.valueOf(start))
+ .setAttribute("end", String.valueOf(start + fileParams.url.length()));
+ }
}
packet.addChild("x", Namespace.OOB).addChild("url").setContent(fileParams.url);
@@ -131,7 +131,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
}
}
- OkHttpClient buildHttpClient(final HttpUrl url, final Account account, boolean interactive) {
+ public OkHttpClient buildHttpClient(final HttpUrl url, final Account account, boolean interactive) {
return buildHttpClient(url, account, 30, interactive);
}
@@ -182,4 +182,31 @@ public class HttpConnectionManager extends AbstractConnectionManager {
}
return body.byteStream();
}
+
+ public static String extractFilenameFromResponse(okhttp3.Response response) {
+ String filename = null;
+
+ // Try to extract filename from the Content-Disposition header
+ String contentDisposition = response.header("Content-Disposition");
+ if (contentDisposition != null && contentDisposition.contains("filename=")) {
+ String[] parts = contentDisposition.split(";");
+ for (String part : parts) {
+ if (part.trim().startsWith("filename=")) {
+ filename = part.substring("filename=".length()).trim().replace("\"", "");
+ break;
+ }
+ }
+ }
+
+ // If filename is not found in the Content-Disposition header, try to get it from the URL
+ if (filename == null || filename.isEmpty()) {
+ HttpUrl httpUrl = response.request().url();
+ List<String> pathSegments = httpUrl.pathSegments();
+ if (!pathSegments.isEmpty()) {
+ filename = pathSegments.get(pathSegments.size() - 1);
+ }
+ }
+
+ return filename;
+ }
}
@@ -213,7 +213,6 @@ public class HttpDownloadConnection implements Transferable {
message.setDeleted(true);
}
message.setTransferable(null);
- if (cb != null) cb.accept(file);
mXmppConnectionService.updateMessage(message);
mHttpConnectionManager.finishConnection(this);
final boolean notifyAfterScan = notify;
@@ -399,6 +398,7 @@ public class HttpDownloadConnection implements Transferable {
decryptIfNeeded();
finish();
updateImageBounds();
+ if (cb != null) cb.accept(file);
} catch (final SSLHandshakeException e) {
changeStatus(STATUS_OFFER);
} catch (final Exception e) {
@@ -71,6 +71,7 @@ import org.openintents.openpgp.util.OpenPgpServiceConnection;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
+import java.net.URI;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
@@ -186,6 +187,9 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
import me.leolin.shortcutbadger.ShortcutBadger;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+
public class XmppConnectionService extends Service {
public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations";
@@ -1638,10 +1642,10 @@ public class XmppConnectionService extends Service {
}
public void sendMessage(final Message message) {
- sendMessage(message, false, false);
+ sendMessage(message, false, false, false);
}
- private void sendMessage(final Message message, final boolean resend, final boolean delay) {
+ private void sendMessage(final Message message, final boolean resend, final boolean previewedLinks, final boolean delay) {
final Account account = message.getConversation().getAccount();
if (account.setShowErrorNotification(true)) {
databaseBackend.updateAccount(account);
@@ -1676,7 +1680,65 @@ public class XmppConnectionService extends Service {
message.setCounterpart(message.getConversation().getJid().asBareJid());
}
- if (account.isOnlineAndConnected() && !inProgressJoin) {
+ boolean waitForPreview = false;
+ if (getPreferences().getBoolean("send_link_previews", true) && !previewedLinks && !message.needsUploading()) {
+ final List<URI> links = message.getLinks();
+ if (!links.isEmpty()) {
+ waitForPreview = true;
+ if (account.isOnlineAndConnected()) {
+ FILE_ATTACHMENT_EXECUTOR.execute(() -> {
+ for (URI link : links) {
+ if ("https".equals(link.getScheme())) {
+ try {
+ HttpUrl url = HttpUrl.parse(link.toString());
+ OkHttpClient http = getHttpConnectionManager().buildHttpClient(url, account, false);
+ okhttp3.Response response = http.newCall(new okhttp3.Request.Builder().url(url).head().build()).execute();
+ final String mimeType = response.header("Content-Type") == null ? "" : response.header("Content-Type");
+ final boolean image = mimeType.startsWith("image/");
+ final boolean audio = mimeType.startsWith("audio/");
+ final boolean video = mimeType.startsWith("video/");
+ final boolean pdf = mimeType.equals("application/pdf");
+ if (response.isSuccessful() && (image || audio || video || pdf)) {
+ Message.FileParams params = message.getFileParams();
+ params.url = url.toString();
+ if (response.header("Content-Length") != null) params.size = Long.parseLong(response.header("Content-Length"), 10);
+ if (!Message.configurePrivateFileMessage(message)) {
+ message.setType(image ? Message.TYPE_IMAGE : Message.TYPE_FILE);
+ }
+ params.setName(HttpConnectionManager.extractFilenameFromResponse(response));
+
+ if (link.toString().equals(message.getQuoteableBody())) {
+ Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
+ fallback.addChild("body", "urn:xmpp:fallback:0");
+ message.addPayload(fallback);
+ } else if (message.getQuoteableBody().indexOf(link.toString()) >= 0) {
+ // Part of the real body, not just a fallback
+ Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
+ fallback.addChild("body", "urn:xmpp:fallback:0")
+ .setAttribute("start", "0")
+ .setAttribute("end", "0");
+ message.addPayload(fallback);
+ }
+
+ getHttpConnectionManager().createNewDownloadConnection(message, false, (file) -> {
+ synchronized (message.getConversation()) {
+ if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false);
+ }
+ });
+ return;
+ }
+ } catch (final IOException e) { }
+ }
+ }
+ synchronized (message.getConversation()) {
+ if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false);
+ }
+ });
+ }
+ }
+ }
+
+ if (account.isOnlineAndConnected() && !inProgressJoin && !waitForPreview) {
switch (message.getEncryption()) {
case Message.ENCRYPTION_NONE:
if (message.needsUploading()) {
@@ -1822,11 +1884,13 @@ public class XmppConnectionService extends Service {
}
private void sendUnsentMessages(final Conversation conversation) {
- conversation.findWaitingMessages(message -> resendMessage(message, true));
+ synchronized (conversation) {
+ conversation.findWaitingMessages(message -> resendMessage(message, true));
+ }
}
public void resendMessage(final Message message, final boolean delay) {
- sendMessage(message, true, delay);
+ sendMessage(message, true, false, delay);
}
public boolean isOnboarding() {
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="show_avatar_incoming_call">true</bool>
-</resources>
+ <bool name="send_link_previews">true</bool>
+</resources>
@@ -1010,6 +1010,8 @@
<string name="pref_up_push_account_summary">The account through which push messages will be received.</string>
<string name="pref_up_push_server_title">Push Server</string>
<string name="pref_up_push_server_summary">A user-chosen push server to relay push messages to apps on your device.</string>
+ <string name="pref_send_link_previews">Send link previews</string>
+ <string name="pref_send_link_previews_summary">Attach metadata about links when sending a message</string>
<string name="no_account_deactivated">None (deactivated)</string>
<string name="decline">Decline</string>
<string name="delete_from_server">Remove account from server</string>
@@ -35,6 +35,12 @@
android:summary="@string/pref_broadcast_last_activity_summary"
android:title="@string/pref_broadcast_last_activity" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/send_link_previews"
+ android:key="send_link_previews"
+ android:summary="@string/pref_send_link_previews_summary"
+ android:title="@string/pref_send_link_previews" />
+
<CheckBoxPreference
android:defaultValue="@bool/prevent_screenshots"
android:key="prevent_screenshots"