diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index ce2953ecbc65362cd260bccdcdfc171896bfb714..220c2aa58281c563d04e4d799f7bc2526b613ace 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -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 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 { diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index d031bdb21e43d33235040ba9062c61f137280c16..709a6b464ac74fba7fb691e1b18fc2e95914a89e 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -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); diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 999c81c467228865fba5102ce1d8f5d5aba9fadc..f94e6d40656faae62c8a6a73e24b0a2ccd81f444 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -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 pathSegments = httpUrl.pathSegments(); + if (!pathSegments.isEmpty()) { + filename = pathSegments.get(pathSegments.size() - 1); + } + } + + return filename; + } } diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index bb7e46923adc62852dfc98a709e49216d6c42880..514e59c261970356756eac63f99b5fa6f4689d53 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -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) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index eddd8e3c3978e43916040a348df354de5c345fda..9527bd8931cbfd3cbc992a457bfebf5bc2df8bea 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -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 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() { diff --git a/src/main/res/values/bools.xml b/src/main/res/values/bools.xml index 0799afb3f53b5938ae611c3cb1110a04e87a6c52..b3628dc8a6758cedd0240e46e2cd4820a562717b 100644 --- a/src/main/res/values/bools.xml +++ b/src/main/res/values/bools.xml @@ -1,4 +1,5 @@ true - \ No newline at end of file + true + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index edcb04abeb05f02896ffa6260a3f69c7004b7fbe..09fc14ba208f421dc61a693f13bb998400958762 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1010,6 +1010,8 @@ The account through which push messages will be received. Push Server A user-chosen push server to relay push messages to apps on your device. + Send link previews + Attach metadata about links when sending a message None (deactivated) Decline Remove account from server diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index ded8abde55dd702efb3f65f38c70dd3f04f4012e..c0a016686c83fdd05baf2c65dacb42a9d223f896 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -35,6 +35,12 @@ android:summary="@string/pref_broadcast_last_activity_summary" android:title="@string/pref_broadcast_last_activity" /> + +