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

This commit is contained in:
Stephen Paul Weber 2023-09-30 21:16:54 -05:00 committed by 12aw
parent 7af92975cc
commit bc249aaa70
26 changed files with 304 additions and 113 deletions

View file

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

View file

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

View file

@ -18,4 +18,5 @@ android.enableJetifier=true
android.useAndroidX=true
org.gradle.gradle-args=--max-workers=32
android.nonTransitiveRClass=true
android.nonFinalResIds=false
android.nonFinalResIds=false
org.gradle.daemon=false

View file

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

View file

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

View file

@ -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<String> pathSegments = httpUrl.pathSegments();
if (!pathSegments.isEmpty()) {
filename = pathSegments.get(pathSegments.size() - 1);
}
}
return filename;
}
}

View file

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

View file

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

View file

@ -88,6 +88,8 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
Log.d(Config.LOGTAG, "mime=" + mime);
if (mime == null) {
attr = R.attr.media_preview_unknown;
} else if (mime.equals("audio/x-m4b")) {
attr = R.attr.media_preview_audiobook;
} else if (mime.startsWith("audio/")) {
attr = R.attr.media_preview_audio;
} else if (mime.equals("text/calendar") || (mime.equals("text/x-vcalendar"))) {

View file

@ -168,7 +168,7 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
}
}
class MediaPreviewViewHolder extends RecyclerView.ViewHolder {
static class MediaPreviewViewHolder extends RecyclerView.ViewHolder {
private final MediaPreviewBinding binding;

View file

@ -32,6 +32,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Arrays;
import java.util.Collection;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@ -262,6 +264,7 @@ public final class MimeUtils {
add("audio/mpeg", "mpega");
add("audio/mpeg", "mp2");
add("audio/mp4", "m4a");
add("audio/x-m4b", "m4b");
add("audio/mpegurl", "m3u");
add("audio/ogg", "ogg");
add("audio/ogg", "oga");
@ -427,6 +430,8 @@ public final class MimeUtils {
"text/plain"
);
// mime types that are more reliant by path
private static final Collection<String> 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) {

View file

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

View file

@ -0,0 +1,6 @@
<vector android:height="48dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18,11c0.34,0 0.67,0.03 1,0.08V4c0,-1.1 -0.9,-2 -2,-2H5C3.9,2 3,2.9 3,4v16c0,1.1 0.9,2 2,2h7.26C11.47,20.87 11,19.49 11,18C11,14.13 14.13,11 18,11zM7,11V4h5v7L9.5,9.5L7,11z"/>
<path android:fillColor="@android:color/white" android:pathData="M18,13c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5S20.76,13 18,13zM16.75,20.5v-5l4,2.5L16.75,20.5z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:height="48dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18,11c0.34,0 0.67,0.03 1,0.08V4c0,-1.1 -0.9,-2 -2,-2H5C3.9,2 3,2.9 3,4v16c0,1.1 0.9,2 2,2h7.26C11.47,20.87 11,19.49 11,18C11,14.13 14.13,11 18,11zM7,11V4h5v7L9.5,9.5L7,11z"/>
<path android:fillColor="@android:color/white" android:pathData="M18,13c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5S20.76,13 18,13zM16.75,20.5v-5l4,2.5L16.75,20.5z"/>
</vector>

View file

@ -2,13 +2,13 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners
android:topLeftRadius="2dp"
android:topRightRadius="15dp"
android:bottomRightRadius="15dp"
android:bottomLeftRadius="15dp" />
android:topRightRadius="20dp"
android:bottomRightRadius="20dp"
android:bottomLeftRadius="20dp" />
<padding
android:bottom="2dp"
android:left="6dp"
android:right="6dp"
android:bottom="0dp"
android:left="10dp"
android:right="10dp"
android:top="2dp" />
<solid android:color="@color/lightblue" />
</shape>

View file

@ -3,13 +3,13 @@
<stroke android:width="2dp" android:color="@color/accent"/>
<corners
android:topLeftRadius="2dp"
android:topRightRadius="15dp"
android:bottomRightRadius="15dp"
android:bottomLeftRadius="15dp" />
android:topRightRadius="20dp"
android:bottomRightRadius="20dp"
android:bottomLeftRadius="20dp" />
<padding
android:bottom="2dp"
android:left="6dp"
android:right="6dp"
android:bottom="0dp"
android:left="10dp"
android:right="10dp"
android:top="2dp" />
<solid android:color="@color/lightblue" />
</shape>

View file

@ -2,13 +2,13 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners
android:topLeftRadius="2dp"
android:topRightRadius="15dp"
android:bottomRightRadius="15dp"
android:bottomLeftRadius="15dp" />
android:topRightRadius="20dp"
android:bottomRightRadius="20dp"
android:bottomLeftRadius="20dp" />
<padding
android:bottom="4dp"
android:left="6dp"
android:right="6dp"
android:bottom="2dp"
android:left="10dp"
android:right="10dp"
android:top="4dp" />
<solid android:color="@color/lightred" />
</shape>

View file

@ -3,13 +3,13 @@
<stroke android:width="2dp" android:color="@color/accent"/>
<corners
android:topLeftRadius="2dp"
android:topRightRadius="15dp"
android:bottomRightRadius="15dp"
android:bottomLeftRadius="15dp" />
android:topRightRadius="20dp"
android:bottomRightRadius="20dp"
android:bottomLeftRadius="20dp" />
<padding
android:bottom="4dp"
android:left="6dp"
android:right="6dp"
android:bottom="2dp"
android:left="10dp"
android:right="10dp"
android:top="4dp" />
<solid android:color="@color/lightred" />
</shape>

View file

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners
android:topLeftRadius="15dp"
android:topRightRadius="15dp"
android:topLeftRadius="20dp"
android:topRightRadius="20dp"
android:bottomRightRadius="2dp"
android:bottomLeftRadius="15dp" />
android:bottomLeftRadius="20dp" />
<padding
android:bottom="4dp"
android:left="6dp"
android:right="6dp"
android:bottom="2dp"
android:left="10dp"
android:right="10dp"
android:top="4dp" />
<solid android:color="@color/darkblue" />
</shape>

View file

@ -2,14 +2,14 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<stroke android:width="2dp" android:color="@color/accent"/>
<corners
android:topLeftRadius="15dp"
android:topRightRadius="15dp"
android:topLeftRadius="20dp"
android:topRightRadius="20dp"
android:bottomRightRadius="2dp"
android:bottomLeftRadius="15dp" />
android:bottomLeftRadius="20dp" />
<padding
android:bottom="4dp"
android:left="6dp"
android:right="6dp"
android:bottom="2dp"
android:left="10dp"
android:right="10dp"
android:top="4dp" />
<solid android:color="@color/darkblue" />
</shape>

View file

@ -409,6 +409,7 @@
<string name="image">Bild</string>
<string name="pdf_document">PDF-Dokument</string>
<string name="apk">Android App</string>
<string name="audiobook">Audiobuch</string>
<string name="vcard">Kontakt</string>
<string name="avatar_has_been_published">Profilbild wurde gespeichert</string>
<string name="sending_x_file">%s wird gesendet</string>

View file

@ -67,6 +67,7 @@
<attr name="media_preview_tour" format="reference" />
<attr name="media_preview_contact" format="reference" />
<attr name="media_preview_app" format="reference" />
<attr name="media_preview_audiobook" format="reference" />
<attr name="media_preview_calendar" format="reference" />
<attr name="media_preview_archive" format="reference" />
<attr name="media_preview_ebook" format="reference" />

View file

@ -10,4 +10,5 @@
<bool name="set_text_collapsable">false</bool>
<bool name="enforce_dane">false</bool>
<bool name="hide_donation_snackbar">false</bool>
<bool name="send_link_previews">true</bool>
</resources>

View file

@ -392,6 +392,7 @@
<string name="image">image</string>
<string name="pdf_document">PDF document</string>
<string name="apk">Android App</string>
<string name="audiobook">Audiobook</string>
<string name="vcard">Contact</string>
<string name="avatar_has_been_published">Avatar has been published!</string>
<string name="sending_x_file">Sending %s</string>
@ -1405,4 +1406,6 @@
<string name="grey">Grey</string>
<string name="blue">Blue</string>
<string name="green_and_blue">Green and blue</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>
</resources>

View file

@ -114,6 +114,7 @@
<item name="media_preview_tour" type="reference">@drawable/baseline_tour_black_48</item>
<item name="media_preview_contact" type="reference">@drawable/ic_person_black_48dp</item>
<item name="media_preview_app" type="reference">@drawable/ic_android_black_48dp</item>
<item name="media_preview_audiobook" type="reference">@drawable/ic_play_lesson_black_24</item>
<item name="media_preview_calendar" type="reference">@drawable/ic_event_black_48dp</item>
<item name="media_preview_archive" type="reference">@drawable/ic_archive_black_48dp</item>
<item name="media_preview_ebook" type="reference">@drawable/ic_book_black_48dp</item>
@ -352,6 +353,7 @@
<item name="media_preview_tour" type="reference">@drawable/baseline_tour_white_48</item>
<item name="media_preview_contact" type="reference">@drawable/ic_person_white_48dp</item>
<item name="media_preview_app" type="reference">@drawable/ic_android_white_48dp</item>
<item name="media_preview_audiobook" type="reference">@drawable/ic_play_lesson_white_48dp</item>
<item name="media_preview_calendar" type="reference">@drawable/ic_event_white_48dp</item>
<item name="media_preview_archive" type="reference">@drawable/ic_archive_white_48dp</item>
<item name="media_preview_ebook" type="reference">@drawable/ic_book_white_48dp</item>

View file

@ -482,6 +482,11 @@
android:key="last_activity"
android:summary="@string/pref_broadcast_last_activity_summary"
android:title="@string/pref_broadcast_last_activity" />
<SwitchPreference
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" />
<SwitchPreference
android:defaultValue="@bool/notifications_from_strangers"
android:key="notifications_from_strangers"
@ -502,25 +507,25 @@
android:key="use_internal_updater"
android:summary="@string/pref_use_internal_updater_summary"
android:title="@string/pref_use_internal_updater" />
<!--
<SwitchPreference
android:defaultValue="@bool/show_links_inside"
android:key="show_links_inside"
android:summary="@string/pref_show_links_inside_summary"
android:title="@string/pref_show_links_inside" />
-->
<SwitchPreference
android:defaultValue="@bool/show_maps_inside"
android:disableDependentsState="false"
android:key="show_maps_inside"
android:summary="@string/pref_show_mappreview_inside_summary"
android:title="@string/pref_show_mappreview_inside" />
<!--
<EditTextPreference
android:defaultValue="@string/mappreview_url"
android:dependency="show_maps_inside"
android:key="mappreview_host"
android:summary="@string/pref_mappreview_host_summary"
android:title="@string/pref_mappreview_host" />
-->
<SwitchPreference
android:defaultValue="@bool/warn_unencrypted_chat"
android:key="warn_unencrypted_chat"