From bc249aaa70b72d68da0d15eaa8dd68c6ee8fd5fa Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Sat, 30 Sep 2023 21:16:54 -0500 Subject: [PATCH] Basic link previews + include opengraph metadata about HTML links + Links in a quote are not really in this message + OpenGraph library not using tor right now, so disable when we are + disable Gradle daemon + Better defaults for OpenGraphParser + Eventually we need to move on + Empty result as an error instead of hanging + re-activate map preview (Experimental) + modify mime type detection for shared files --- build.gradle | 3 +- git/release/output-metadata.json | 26 ++-- gradle.properties | 3 +- .../siacs/conversations/entities/Message.java | 17 ++- .../generator/MessageGenerator.java | 24 ++-- .../http/HttpConnectionManager.java | 31 ++++- .../http/HttpDownloadConnection.java | 2 +- .../services/XmppConnectionService.java | 126 ++++++++++++++++-- .../ui/adapter/MediaAdapter.java | 2 + .../ui/adapter/MediaPreviewAdapter.java | 2 +- .../siacs/conversations/utils/MimeUtils.java | 78 ++++++----- .../siacs/conversations/utils/UIHelper.java | 2 + .../res/drawable/ic_play_lesson_black_24.xml | 6 + .../drawable/ic_play_lesson_white_48dp.xml | 6 + .../message_bubble_received_light.xml | 12 +- .../message_bubble_received_light_private.xml | 12 +- .../message_bubble_received_warning.xml | 12 +- ...essage_bubble_received_warning_private.xml | 12 +- src/main/res/drawable/message_bubble_sent.xml | 12 +- .../drawable/message_bubble_sent_private.xml | 12 +- src/main/res/values-de-rDE/strings.xml | 1 + src/main/res/values/attrs.xml | 1 + src/main/res/values/bools.xml | 1 + src/main/res/values/strings.xml | 3 + src/main/res/values/themes.xml | 2 + src/main/res/xml/preferences.xml | 9 +- 26 files changed, 304 insertions(+), 113 deletions(-) create mode 100644 src/main/res/drawable/ic_play_lesson_black_24.xml create mode 100644 src/main/res/drawable/ic_play_lesson_white_48dp.xml diff --git a/build.gradle b/build.gradle index 202f81e23..03b54b135 100644 --- a/build.gradle +++ b/build.gradle @@ -128,8 +128,9 @@ dependencies { implementation "com.daimajia.swipelayout:library:1.2.0@aar" implementation 'com.nineoldandroids:library:2.4.0' implementation "androidx.core:core-ktx:1.12.0" - implementation "androidx.compose.material3:material3-android:1.2.0-beta02" + implementation "androidx.compose.material3:material3-android:1.2.0-rc01" implementation "androidx.emoji2:emoji2-emojipicker:1.4.0" + implementation 'com.github.Priyansh-Kedia:OpenGraphParser:2.5.6' } ext { diff --git a/git/release/output-metadata.json b/git/release/output-metadata.json index fa0dc1901..b3185e81d 100644 --- a/git/release/output-metadata.json +++ b/git/release/output-metadata.json @@ -15,19 +15,6 @@ "versionName": "1.7.8.8", "outputFile": "monocles chat-1.7.8.8-git-universal-release.apk" }, - { - "type": "ONE_OF_MANY", - "filters": [ - { - "filterType": "ABI", - "value": "armeabi-v7a" - } - ], - "attributes": [], - "versionCode": 15901, - "versionName": "1.7.8.8", - "outputFile": "monocles chat-1.7.8.8-git-armeabi-v7a-release.apk" - }, { "type": "ONE_OF_MANY", "filters": [ @@ -41,6 +28,19 @@ "versionName": "1.7.8.8", "outputFile": "monocles chat-1.7.8.8-git-arm64-v8a-release.apk" }, + { + "type": "ONE_OF_MANY", + "filters": [ + { + "filterType": "ABI", + "value": "armeabi-v7a" + } + ], + "attributes": [], + "versionCode": 15901, + "versionName": "1.7.8.8", + "outputFile": "monocles chat-1.7.8.8-git-armeabi-v7a-release.apk" + }, { "type": "ONE_OF_MANY", "filters": [ diff --git a/gradle.properties b/gradle.properties index 405a7bc0e..cd8a7cb3c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,4 +18,5 @@ android.enableJetifier=true android.useAndroidX=true org.gradle.gradle-args=--max-workers=32 android.nonTransitiveRClass=true -android.nonFinalResIds=false \ No newline at end of file +android.nonFinalResIds=false +org.gradle.daemon=false \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index a09355be4..de6f5395f 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -15,6 +15,8 @@ import android.util.Log; import android.util.Base64; import android.util.Pair; import android.view.View; + +import eu.siacs.conversations.ui.util.MyLinkify; import eu.siacs.conversations.utils.Compatibility; import android.graphics.drawable.Drawable; import android.text.Html; @@ -918,7 +920,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 { @@ -1203,6 +1205,19 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } } + public List getLinks() { + SpannableStringBuilder text = new SpannableStringBuilder( + getBody().replaceAll("^>.*", "") // Remove quotes + ); + return MyLinkify.extractLinks(text).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 1458426cb..f1059d144 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -150,17 +150,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 268da54fb..8aeb7278f 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -129,11 +129,11 @@ 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); } - OkHttpClient buildHttpClient(final HttpUrl url, final Account account, int readTimeout, boolean interactive) { + public OkHttpClient buildHttpClient(final HttpUrl url, final Account account, int readTimeout, boolean interactive) { final String slotHostname = url.host(); final boolean onionSlot = slotHostname.endsWith(".onion"); final boolean I2PSlot = slotHostname.endsWith(".i2p"); @@ -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; + } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 4bc96178e..967a87d4f 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -232,7 +232,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; @@ -419,6 +418,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 f54d440ca..33a883b40 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -23,12 +23,17 @@ import android.graphics.drawable.AnimatedImageDrawable; import android.provider.DocumentsContract; import com.google.common.io.Files; +import com.kedia.ogparser.JsoupProxy; +import com.kedia.ogparser.OpenGraphCallback; +import com.kedia.ogparser.OpenGraphParser; +import com.kedia.ogparser.OpenGraphResult; + import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.persistance.UnifiedPushDatabase; import eu.siacs.conversations.xmpp.OnGatewayResult; import eu.siacs.conversations.utils.Consumer; - +import java.net.URI; import static eu.siacs.conversations.utils.Compatibility.s; import android.Manifest; import androidx.annotation.RequiresApi; @@ -138,6 +143,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; @@ -232,6 +238,9 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; import io.ipfs.cid.Cid; import me.leolin.shortcutbadger.ShortcutBadger; +import java.util.concurrent.TimeUnit; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; public class XmppConnectionService extends Service { @@ -2097,10 +2106,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) { if (resend) { message.setTime(System.currentTimeMillis()); } @@ -2140,7 +2149,108 @@ 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"); + final boolean html = mimeType.startsWith("text/html") || mimeType.startsWith("application/xhtml+xml"); + 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; + } else if (response.isSuccessful() && html && !useI2PToConnect()) { + Semaphore waiter = new Semaphore(0); + OpenGraphParser.Builder openGraphBuilder = new OpenGraphParser.Builder(new OpenGraphCallback() { + @Override + public void onPostResponse(OpenGraphResult result) { + Element rdf = new Element("Description", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); + rdf.setAttribute("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); + rdf.setAttribute("rdf:about", link.toString()); + if (result.getTitle() != null && !"".equals(result.getTitle())) { + rdf.addChild("title", "https://ogp.me/ns#").setContent(result.getTitle()); + } + if (result.getDescription() != null && !"".equals(result.getDescription())) { + rdf.addChild("description", "https://ogp.me/ns#").setContent(result.getDescription()); + } + if (result.getUrl() != null) { + rdf.addChild("url", "https://ogp.me/ns#").setContent(result.getUrl()); + } + if (result.getImage() != null) { + rdf.addChild("image", "https://ogp.me/ns#").setContent(result.getImage()); + } + if (result.getType() != null) { + rdf.addChild("type", "https://ogp.me/ns#").setContent(result.getType()); + } + if (result.getSiteName() != null) { + rdf.addChild("site_name", "https://ogp.me/ns#").setContent(result.getSiteName()); + } + message.addPayload(rdf); + waiter.release(); + } + + public void onError(String error) { + waiter.release(); + } + }) + .showNullOnEmpty(true) + .maxBodySize(4000) + .timeout(5000); + if (useTorToConnect()) { + openGraphBuilder = openGraphBuilder.jsoupProxy(new JsoupProxy("127.0.0.1", 8118)); + } + openGraphBuilder.build().parse(link.toString()); + waiter.tryAcquire(10L, TimeUnit.SECONDS); + } + } catch (final IOException | InterruptedException 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()) { @@ -2317,11 +2427,9 @@ public class XmppConnectionService extends Service { } private void sendUnsentMessages(final Conversation conversation) { - final Runnable runnable = () -> { + synchronized (conversation) { conversation.findWaitingMessages(message -> resendMessage(message, true)); - }; - mDatabaseWriterExecutor.execute((runnable)); - + } } private void resendFailedMessages(final Conversation conversation) { final Runnable runnable = () -> { @@ -2348,7 +2456,7 @@ public class XmppConnectionService extends Service { } public void resendMessage(final Message message, final boolean delay) { - sendMessage(message, true, delay); + sendMessage(message, true, false, delay); } public void requestEasyOnboardingInvite(final Account account, final EasyOnboardingInvite.OnInviteRequested callback) { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java index 768494465..8aa1c9d7a 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java @@ -88,6 +88,8 @@ public class MediaAdapter extends RecyclerView.Adapter PATH_PRECEDENCE_MIME_TYPE = Arrays.asList("audio/x-m4b"); private static void add(String mimeType, String extension) { // If we have an existing x -> y mapping, we do not want to @@ -552,46 +557,49 @@ public final class MimeUtils { } public static String guessMimeTypeFromUriAndMime(final Context context, final Uri uri, final String mime) { - Log.d(Config.LOGTAG, "guessMimeTypeFromUriAndMime " + uri + " and mime=" + mime); - if (mime == null || mime.equals("application/octet-stream")) { - final String guess = guessMimeTypeFromUri(context, uri); - if (guess != null) { - return guess; - } else { - return mime; - } + Log.d(Config.LOGTAG, "guessMimeTypeFromUriAndMime(" + uri + "," + mime+")"); + final String mimeFromUri = guessMimeTypeFromUri(context, uri); + Log.d(Config.LOGTAG,"mimeFromUri:"+mimeFromUri); + if (PATH_PRECEDENCE_MIME_TYPE.contains(mimeFromUri)) { + return mimeFromUri; + } else if (mime == null || mime.equals("application/octet-stream")) { + return mimeFromUri; + } else { + return mime; } - return guessMimeTypeFromUri(context, uri); } - public static String guessMimeTypeFromUri(Context context, Uri uri) { - // try the content resolver - String mimeType; + public static String guessMimeTypeFromUri(final Context context, final Uri uri) { + final String mimeTypeContentResolver = guessFromContentResolver(context, uri); + final String mimeTypeFromQueryParameter = uri.getQueryParameter("mimeType"); + final String name = "content".equals(uri.getScheme()) ? getDisplayName(context, uri) : null; + final String mimeTypeFromName = Strings.isNullOrEmpty(name) ? null : guessFromPath(name); + final String path = uri.getPath(); + final String mimeTypeFromPath = Strings.isNullOrEmpty(path) ? null : guessFromPath(path); + if (PATH_PRECEDENCE_MIME_TYPE.contains(mimeTypeFromName)) { + return mimeTypeFromName; + } + if (PATH_PRECEDENCE_MIME_TYPE.contains(mimeTypeFromPath)) { + return mimeTypeFromPath; + } + if (mimeTypeContentResolver != null && !"application/octet-stream".equals(mimeTypeContentResolver)) { + return mimeTypeContentResolver; + } + if (mimeTypeFromName != null) { + return mimeTypeFromName; + } + if (mimeTypeFromQueryParameter != null) { + return mimeTypeFromQueryParameter; + } + return mimeTypeFromPath; + } + + private static String guessFromContentResolver(final Context context, final Uri uri) { try { - mimeType = context.getContentResolver().getType(uri); - } catch (final Throwable throwable) { - mimeType = null; + return context.getContentResolver().getType(uri); + } catch (final Throwable e) { + return null; } - // try the extension - if (mimeType == null || mimeType.equals("application/octet-stream")) { - final String path = uri.getPath(); - if (path != null) { - mimeType = guessFromPath(path); - } - } - if (mimeType == null && "content".equals(uri.getScheme())) { - final String name = getDisplayName(context, uri); - if (name != null) { - mimeType = guessFromPath(name); - } - } - // sometimes this works (as with the commit content api) - if (mimeType == null) { - try { - mimeType = uri.getQueryParameter("mimeType"); - } catch (final Throwable throwable) { } - } - return mimeType; } private static String getDisplayName(final Context context, final Uri uri) { diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index f2bfbf590..6562cea84 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -541,6 +541,8 @@ public class UIHelper { return context.getString(R.string.file); } else if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) { return context.getString(R.string.multimedia_file); + } else if (mime.equals("audio/x-m4b")) { + return context.getString(R.string.audiobook); } else if (mime.startsWith("audio/")) { return context.getString(R.string.audio); } else if (mime.startsWith("video/")) { diff --git a/src/main/res/drawable/ic_play_lesson_black_24.xml b/src/main/res/drawable/ic_play_lesson_black_24.xml new file mode 100644 index 000000000..4c4a46ce7 --- /dev/null +++ b/src/main/res/drawable/ic_play_lesson_black_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/main/res/drawable/ic_play_lesson_white_48dp.xml b/src/main/res/drawable/ic_play_lesson_white_48dp.xml new file mode 100644 index 000000000..67fe7c696 --- /dev/null +++ b/src/main/res/drawable/ic_play_lesson_white_48dp.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/main/res/drawable/message_bubble_received_light.xml b/src/main/res/drawable/message_bubble_received_light.xml index af8ea4fed..0158e8c35 100644 --- a/src/main/res/drawable/message_bubble_received_light.xml +++ b/src/main/res/drawable/message_bubble_received_light.xml @@ -2,13 +2,13 @@ + android:topRightRadius="20dp" + android:bottomRightRadius="20dp" + android:bottomLeftRadius="20dp" /> \ No newline at end of file diff --git a/src/main/res/drawable/message_bubble_received_light_private.xml b/src/main/res/drawable/message_bubble_received_light_private.xml index fbaa927f2..5bab3da0f 100644 --- a/src/main/res/drawable/message_bubble_received_light_private.xml +++ b/src/main/res/drawable/message_bubble_received_light_private.xml @@ -3,13 +3,13 @@ + android:topRightRadius="20dp" + android:bottomRightRadius="20dp" + android:bottomLeftRadius="20dp" /> \ No newline at end of file diff --git a/src/main/res/drawable/message_bubble_received_warning.xml b/src/main/res/drawable/message_bubble_received_warning.xml index 70b4ed4c0..88c7b255d 100644 --- a/src/main/res/drawable/message_bubble_received_warning.xml +++ b/src/main/res/drawable/message_bubble_received_warning.xml @@ -2,13 +2,13 @@ + android:topRightRadius="20dp" + android:bottomRightRadius="20dp" + android:bottomLeftRadius="20dp" /> \ No newline at end of file diff --git a/src/main/res/drawable/message_bubble_received_warning_private.xml b/src/main/res/drawable/message_bubble_received_warning_private.xml index 080709d7d..3a7754c0a 100644 --- a/src/main/res/drawable/message_bubble_received_warning_private.xml +++ b/src/main/res/drawable/message_bubble_received_warning_private.xml @@ -3,13 +3,13 @@ + android:topRightRadius="20dp" + android:bottomRightRadius="20dp" + android:bottomLeftRadius="20dp" /> \ No newline at end of file diff --git a/src/main/res/drawable/message_bubble_sent.xml b/src/main/res/drawable/message_bubble_sent.xml index f97b52716..f778ada11 100644 --- a/src/main/res/drawable/message_bubble_sent.xml +++ b/src/main/res/drawable/message_bubble_sent.xml @@ -1,14 +1,14 @@ + android:bottomLeftRadius="20dp" /> \ No newline at end of file diff --git a/src/main/res/drawable/message_bubble_sent_private.xml b/src/main/res/drawable/message_bubble_sent_private.xml index d2c0902bd..e7d891e99 100644 --- a/src/main/res/drawable/message_bubble_sent_private.xml +++ b/src/main/res/drawable/message_bubble_sent_private.xml @@ -2,14 +2,14 @@ + android:bottomLeftRadius="20dp" /> \ No newline at end of file diff --git a/src/main/res/values-de-rDE/strings.xml b/src/main/res/values-de-rDE/strings.xml index e2b7ecc6b..0c240a9a1 100644 --- a/src/main/res/values-de-rDE/strings.xml +++ b/src/main/res/values-de-rDE/strings.xml @@ -409,6 +409,7 @@ Bild PDF-Dokument Android App + Audiobuch Kontakt Profilbild wurde gespeichert %s wird gesendet diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index 43fe1b49e..857ff9a60 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -67,6 +67,7 @@ + diff --git a/src/main/res/values/bools.xml b/src/main/res/values/bools.xml index ca9f0bfe2..23de58604 100644 --- a/src/main/res/values/bools.xml +++ b/src/main/res/values/bools.xml @@ -10,4 +10,5 @@ false false false + true \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 75825a44a..ee94bc8a1 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -392,6 +392,7 @@ image PDF document Android App + Audiobook Contact Avatar has been published! Sending %s @@ -1405,4 +1406,6 @@ Grey Blue Green and blue + Send link previews + Attach metadata about links when sending a message diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 9de3a16cc..71e1ae4d2 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -114,6 +114,7 @@ @drawable/baseline_tour_black_48 @drawable/ic_person_black_48dp @drawable/ic_android_black_48dp + @drawable/ic_play_lesson_black_24 @drawable/ic_event_black_48dp @drawable/ic_archive_black_48dp @drawable/ic_book_black_48dp @@ -352,6 +353,7 @@ @drawable/baseline_tour_white_48 @drawable/ic_person_white_48dp @drawable/ic_android_white_48dp + @drawable/ic_play_lesson_white_48dp @drawable/ic_event_white_48dp @drawable/ic_archive_white_48dp @drawable/ic_book_white_48dp diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 61a59e4df..d5f106032 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -482,6 +482,11 @@ android:key="last_activity" android:summary="@string/pref_broadcast_last_activity_summary" android:title="@string/pref_broadcast_last_activity" /> + + -