diff --git a/CHANGELOG.md b/CHANGELOG.md index 73f0f42f3..c74a98a73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Changelog -## Version 2.5.8 +### Version 2.5.11 +* Fixed crash on Android <5.0 + +### Version 2.5.10 +* Fixed crash on Xiaomi devices running Android 8.0 + 8.1 + +### Version 2.5.9 +* fixed minor security issues +* Share XMPP uri from channel search by long pressing a result + +### Version 2.5.8 * fixed connection issues over Tor * P2P file transfer (Jingle) now offers direct candidates * Support XEP-0396: Jingle Encrypted Transports - OMEMO diff --git a/README.md b/README.md index 1907d5d45..7a25b3fe1 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,11 @@ Conversations is trying to get rid of old behaviours and set an example for other clients. #### Translations -Translations are managed on [Transifex](https://www.transifex.com/projects/p/conversations/) +Translations are managed on [Transifex](https://www.transifex.com/projects/p/conversations/). +If you want to become a translator Please register on transifex, apply to join +the translation team and then step by our group chat on +[conversations@conference.siacs.eu](https://conversations.im/j/conversations@conference.siacs.eu) +and introduce yourself to `iNPUTmice` so he can approve your join request. #### How do I backup / move Conversations to a new device? On the one hand Conversations supports Message Archive Management to keep a server side history of your messages so when migrating to a new device that device can display your entire history. However that does not work if you enable OMEMO due to its forward secrecy. (Read [The State of Mobile XMPP in 2016](https://gultsch.de/xmpp_2016.html) especially the section on encryption.) diff --git a/build.gradle b/build.gradle index 1eed97d8f..30a942a59 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:3.5.0' } } @@ -63,13 +63,14 @@ dependencies { implementation project(':libs:xmpp-addr') implementation 'org.osmdroid:osmdroid-android:6.1.0' implementation 'org.hsluv:hsluv:0.2' - implementation 'org.conscrypt:conscrypt-android:2.1.0' + implementation 'org.conscrypt:conscrypt-android:2.2.1' implementation 'me.drakeet.support:toastcompat:1.1.0' implementation "com.leinardi.android:speed-dial:2.0.1" - implementation 'com.squareup.retrofit2:retrofit:2.6.0' - implementation 'com.squareup.retrofit2:converter-gson:2.6.0' + implementation 'com.squareup.retrofit2:retrofit:2.6.1' + implementation 'com.squareup.retrofit2:converter-gson:2.6.1' + implementation 'com.squareup.okhttp3:okhttp:3.12.5' implementation 'com.google.guava:guava:27.1-android' - quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.10.1' + quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.10.16' } ext { @@ -83,8 +84,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 25 - versionCode 338 - versionName "2.5.8" + versionCode 341 + versionName "2.5.11" archivesBaseName += "-$versionName" applicationId "eu.sum7.conversations" resValue "string", "applicationId", applicationId diff --git a/doap.rdf b/doap.rdf new file mode 100644 index 000000000..40b5c1884 --- /dev/null +++ b/doap.rdf @@ -0,0 +1,340 @@ + + + + + Conversations + + 2014-01-14 + + Android XMPP Client + + Conversations is an open source XMPP/Jabber client for the Android platform + + + + + + + + + + + + en + + + + Java + + Android + + + + + + + + + Daniel Gultsch + + + + + + + + + + + + + + + + + + + + complete + 1.4 + + + + + + complete + 2.5rc3 + + + + + + complete + 1.32.0 + + + + + + complete + 1.1 + + + + + + complete + 1.1.3 + + + + + + complete + 2.1 + + + + + + complete + 1.1 + + + + + + complete + 1.5.1 + + + + + + complete + 1.2.1 + Avatar, Nick, OMEMO + + + + + + complete + 1.1.2 + File transfer only + + + + + + complete + 1.1 + read only + + + + + + complete + 1.4.0 + + + + + + complete + 1.3 + + + + + + complete + 1.6 + + + + + + complete + 2.0.1 + + + + + + complete + 0.19.1 + + + + + + complete + 1.3 + + + + + + complete + 1.0 + + + + + + complete + 1.2 + + + + + + complete + 1.0.3 + + + + + + complete + 1.0 + + + + + + complete + 0.13.1 + + + + + + complete + 1.0 + + + + + + complete + 0.6.3 + + + + + + complete + 1.0.2 + opt-in + + + + + + complete + 0.3 + + + + + + complete + 0.3.0 + + + + + + complete + 0.4.0 + Only available in the version distributed over Google Play + + + + + + complete + 1.1.0 + + + + + + complete + 0.2 + + + + + + complete + 0.3.0 + + + + + + complete + 0.1.2 + 2.5.8 + + + + + + complete + 0.6.0 + 2.3.1 + + + + + + complete + 0.1.4 + 1.22.0 + + + + + + complete + 0.1 + 2.5.8 + + + + + + complete + 0.2.1 + + + + + + complete + 1.0.1 + 2.5.4 + + + + + + complete + 0.2.0 + + + + + + 2.5.8 + 2019-09-12 + + + + + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f0ff44317..79bc0165e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/proguard-rules.pro b/proguard-rules.pro index 48bbfecbe..5683316d4 100644 --- a/proguard-rules.pro +++ b/proguard-rules.pro @@ -21,6 +21,9 @@ -dontwarn java.lang.** -dontwarn javax.lang.** +-keepclassmembers class eu.siacs.conversations.http.services.** { + !transient ; +} # Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and # EnclosingMethod is required to use InnerClasses. diff --git a/src/conversations/res/values-ar/strings.xml b/src/conversations/res/values-ar/strings.xml new file mode 100644 index 000000000..1e820a9cc --- /dev/null +++ b/src/conversations/res/values-ar/strings.xml @@ -0,0 +1,6 @@ + + + اختر مزود خدمة XMPP الخاص بك + استخدِم conversations.im + أنشئ حسابًا جديدًا + \ No newline at end of file diff --git a/src/conversations/res/values-sv/strings.xml b/src/conversations/res/values-sv/strings.xml new file mode 100644 index 000000000..9212ad109 --- /dev/null +++ b/src/conversations/res/values-sv/strings.xml @@ -0,0 +1,5 @@ + + + Använd conversations.im + Skapa nytt konto + \ No newline at end of file diff --git a/src/conversations/res/values-zh-rCN/strings.xml b/src/conversations/res/values-zh-rCN/strings.xml new file mode 100644 index 000000000..ff6a0c393 --- /dev/null +++ b/src/conversations/res/values-zh-rCN/strings.xml @@ -0,0 +1,7 @@ + + + 选择您的XMPP提供者 + 使用 conversations.im + 创建新账户 + 您已经拥有一个XMPP账户了吗?如果您之前使用过其他的XMPP客户端的话,那么您已经拥有这种账户了。如果没有账户的话,您可以现在创建一个。\n提示:有些电子邮件服务也提供XMPP账户。 + \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 2c9f207d2..e138a80ae 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -116,6 +116,8 @@ public final class Config { public static final boolean IGNORE_ID_REWRITE_IN_MUC = true; public static final boolean MUC_LEAVE_BEFORE_JOIN = true; + public static final boolean USE_LMC_VERSION_1_1 = false; + public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY * 5; public static final int MAM_MAX_MESSAGES = 750; diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index bbc96811b..b804f4222 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -48,7 +48,6 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.SerialSingleThreadExecutor; import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.pep.PublishOptions; @@ -67,8 +66,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public static final String LOGPREFIX = "AxolotlService"; - public static final int NUM_KEYS_TO_PUBLISH = 100; - public static final int publishTriesThreshold = 3; + private static final int NUM_KEYS_TO_PUBLISH = 100; + private static final int publishTriesThreshold = 3; private final Account account; private final XmppConnectionService mXmppConnectionService; @@ -1469,7 +1468,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } else { Log.d(Config.LOGTAG,account.getJid().asBareJid()+": nothing to flush. Not republishing key"); } - completeSession(session); + if (trustedOrPreviouslyResponded(session)) { + completeSession(session); + } } } @@ -1479,23 +1480,44 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { publishBundlesIfNeeded(false, false); } } - Iterator iterator = postponedSessions.iterator(); + final Iterator iterator = postponedSessions.iterator(); while (iterator.hasNext()) { - completeSession(iterator.next()); + final XmppAxolotlSession session = iterator.next(); + if (trustedOrPreviouslyResponded(session)) { + completeSession(session); + } iterator.remove(); } - Iterator postponedHealingAttemptsIterator = postponedHealing.iterator(); + final Iterator postponedHealingAttemptsIterator = postponedHealing.iterator(); while (postponedHealingAttemptsIterator.hasNext()) { notifyRequiresHealing(postponedHealingAttemptsIterator.next()); postponedHealingAttemptsIterator.remove(); } } + + private boolean trustedOrPreviouslyResponded(XmppAxolotlSession session) { + try { + return trustedOrPreviouslyResponded(Jid.of(session.getRemoteAddress().getName())); + } catch (IllegalArgumentException e) { + return false; + } + } + + public boolean trustedOrPreviouslyResponded(Jid jid) { + final Contact contact = account.getRoster().getContact(jid); + if (contact.showInRoster() || contact.isSelf()) { + return true; + } + final Conversation conversation = mXmppConnectionService.find(account, jid); + return conversation != null && conversation.sentMessagesCount() > 0; + } + private void completeSession(XmppAxolotlSession session) { final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); axolotlMessage.addDevice(session, true); try { - Jid jid = Jid.of(session.getRemoteAddress().getName()); + final Jid jid = Jid.of(session.getRemoteAddress().getName()); MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage); mXmppConnectionService.sendMessagePacket(account, packet); } catch (IllegalArgumentException e) { @@ -1505,9 +1527,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) { - XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; - - XmppAxolotlSession session = getReceivingSession(message); + final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; + final XmppAxolotlSession session = getReceivingSession(message); try { keyTransportMessage = message.getParameters(session, getOwnDeviceId()); Integer preKeyId = session.getPreKeyIdAndReset(); @@ -1516,7 +1537,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } } catch (CryptoFailedException e) { Log.d(Config.LOGTAG, "could not decrypt keyTransport message " + e.getMessage()); - keyTransportMessage = null; + return null; } if (session.isFresh() && keyTransportMessage != null) { @@ -1527,7 +1548,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private void putFreshSession(XmppAxolotlSession session) { - Log.d(Config.LOGTAG, "put fresh session"); sessions.put(session); if (Config.X509_VERIFICATION) { if (session.getIdentityKey() != null) { diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index 4dc904ce1..853dc4bab 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -135,7 +135,7 @@ public class XmppAxolotlMessage { break; } } - Element payloadElement = axolotlMessage.findChild(PAYLOAD); + final Element payloadElement = axolotlMessage.findChildEnsureSingle(PAYLOAD, AxolotlService.PEP_PREFIX); if (payloadElement != null) { ciphertext = Base64.decode(payloadElement.getContent().trim(), Base64.DEFAULT); } diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 2004953ee..bd0d4bcd2 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; -import android.graphics.Color; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; @@ -13,23 +12,19 @@ import org.json.JSONObject; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.ListIterator; -import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.OmemoSetting; import eu.siacs.conversations.crypto.PgpDecryptionService; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.utils.JidHelper; import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.mam.MamReference; import rocks.xmpp.addr.Jid; @@ -311,11 +306,12 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) { synchronized (this.messages) { for (int i = this.messages.size() - 1; i >= 0; --i) { - Message message = messages.get(i); + final Message message = messages.get(i); if (counterpart.equals(message.getCounterpart()) && ((message.getStatus() == Message.STATUS_RECEIVED) == received) && (carbon == message.isCarbon() || received)) { - if (id.equals(message.getRemoteMsgId()) && !message.isFileOrImage() && !message.treatAsDownloadable()) { + final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id); + if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) { return message; } else { return null; diff --git a/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java b/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java index 42496e4eb..9a3ae6236 100644 --- a/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java +++ b/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java @@ -43,8 +43,8 @@ public class IndividualMessage extends Message { super(conversation); } - private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, String edited, boolean oob, String errorMessage, Set readByMarkers, boolean markable, boolean deleted) { - super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable, deleted); + private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, String edited, boolean oob, String errorMessage, Set readByMarkers, boolean markable, boolean deleted, String bodyLanguage) { + super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable, deleted, bodyLanguage); } @Override @@ -116,6 +116,8 @@ public class IndividualMessage extends Message { cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)), ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))), cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0, - cursor.getInt(cursor.getColumnIndex(DELETED)) > 0); + cursor.getInt(cursor.getColumnIndex(DELETED)) > 0, + cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)) + ); } } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 02fa07818..2035d8ac8 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -62,6 +62,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public static final String COUNTERPART = "counterpart"; public static final String TRUE_COUNTERPART = "trueCounterpart"; public static final String BODY = "body"; + public static final String BODY_LANGUAGE = "bodyLanguage"; public static final String TIME_SENT = "timeSent"; public static final String ENCRYPTION = "encryption"; public static final String STATUS = "status"; @@ -100,6 +101,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable protected String relativeFilePath; protected boolean read = true; protected String remoteMsgId = null; + private String bodyLanguage = null; protected String serverMsgId = null; private final Conversational conversation; protected Transferable transferable = null; @@ -145,7 +147,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable null, null, false, - false); + false, + null); } protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart, @@ -154,7 +157,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable final String remoteMsgId, final String relativeFilePath, final String serverMsgId, final String fingerprint, final boolean read, final String edited, final boolean oob, final String errorMessage, final Set readByMarkers, - final boolean markable, final boolean deleted) { + final boolean markable, final boolean deleted, final String bodyLanguage) { this.conversation = conversation; this.uuid = uuid; this.conversationUuid = conversationUUid; @@ -177,6 +180,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.readByMarkers = readByMarkers == null ? new HashSet<>() : readByMarkers; this.markable = markable; this.deleted = deleted; + this.bodyLanguage = bodyLanguage; } public static Message fromCursor(Cursor cursor, Conversation conversation) { @@ -201,7 +205,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)), ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))), cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0, - cursor.getInt(cursor.getColumnIndex(DELETED)) > 0); + cursor.getInt(cursor.getColumnIndex(DELETED)) > 0, + cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)) + ); } private static Jid fromString(String value) { @@ -266,6 +272,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString()); values.put(MARKABLE, markable ? 1 : 0); values.put(DELETED, deleted ? 1 : 0); + values.put(BODY_LANGUAGE, bodyLanguage); return values; } @@ -430,6 +437,23 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.edits.add(new Edited(edited, serverMsgId)); } + public boolean remoteMsgIdMatchInEdit(String id) { + for(Edited edit : this.edits) { + if (id.equals(edit.getEditedId())) { + return true; + } + } + return false; + } + + public String getBodyLanguage() { + return this.bodyLanguage; + } + + public void setBodyLanguage(String language) { + this.bodyLanguage = language; + } + public boolean edited() { return this.edits.size() > 0; } @@ -717,6 +741,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } } + public String getEditedIdWireFormat() { + if (edits.size() > 0) { + return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId(); + } else { + throw new IllegalStateException("Attempting to store unedited message"); + } + } + public void setOob(boolean isOob) { this.oob = isOob; } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 771ebccdc..dbf8e7cb2 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -58,7 +58,7 @@ public class MessageGenerator extends AbstractGenerator { packet.setId(message.getUuid()); packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid()); if (message.edited()) { - packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedId()); + packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat()); } return packet; } @@ -193,6 +193,10 @@ public class MessageGenerator extends AbstractGenerator { if (password != null) { x.setAttribute("password", password); } + if (contact.isFullJid()) { + packet.addChild("no-store", "urn:xmpp:hints"); + packet.addChild("no-copy", "urn:xmpp:hints"); + } return packet; } diff --git a/src/main/java/eu/siacs/conversations/http/services/MuclumbusService.java b/src/main/java/eu/siacs/conversations/http/services/MuclumbusService.java index 4f2b8f533..89a8e0ec4 100644 --- a/src/main/java/eu/siacs/conversations/http/services/MuclumbusService.java +++ b/src/main/java/eu/siacs/conversations/http/services/MuclumbusService.java @@ -4,7 +4,6 @@ import com.google.common.base.Objects; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Set; import eu.siacs.conversations.services.AvatarService; @@ -83,7 +82,7 @@ public interface MuclumbusService { class SearchRequest { - public Set keywords; + public final Set keywords; public SearchRequest(String keyword) { this.keywords = Collections.singleton(keyword); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 33179b851..1e7aacb1d 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -33,6 +33,7 @@ import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xml.LocalizedContent; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.InvalidJid; @@ -124,8 +125,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone); } catch (BrokenSessionException e) { if (checkedForDuplicates) { - service.reportBrokenSessionException(e, postpone); - return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status); + if (service.trustedOrPreviouslyResponded(from.asBareJid())) { + service.reportBrokenSessionException(e, postpone); + return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status); + } else { + Log.d(Config.LOGTAG, "ignoring broken session exception because contact was not trusted"); + return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status); + } } else { Log.d(Config.LOGTAG,"ignoring broken session exception because checkForDuplicates failed"); //TODO should be still emit a failed message? @@ -147,31 +153,28 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return null; } - private Invite extractInvite(Account account, Element message) { - Element x = message.findChild("x", "http://jabber.org/protocol/muc#user"); - if (x != null) { - Element invite = x.findChild("invite"); + private Invite extractInvite(Element message) { + final Element mucUser = message.findChild("x", Namespace.MUC_USER); + if (mucUser != null) { + Element invite = mucUser.findChild("invite"); if (invite != null) { - String password = x.findChildContent("password"); + String password = mucUser.findChildContent("password"); Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from")); - Contact contact = from == null ? null : account.getRoster().getContact(from); Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from")); if (room == null) { return null; } - return new Invite(room, password, contact); + return new Invite(room, password, false, from); } - } else { - x = message.findChild("x", "jabber:x:conference"); - if (x != null) { - Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from")); - Contact contact = from == null ? null : account.getRoster().getContact(from); - Jid room = InvalidJid.getNullForInvalid(x.getAttributeAsJid("jid")); - if (room == null) { - return null; - } - return new Invite(room, x.getAttribute("password"), contact); + } + final Element conference = message.findChild("x", "jabber:x:conference"); + if (conference != null) { + Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from")); + Jid room = InvalidJid.getNullForInvalid(conference.getAttributeAsJid("jid")); + if (room == null) { + return null; } + return new Invite(room, conference.getAttribute("password"), true, from); } return null; } @@ -328,8 +331,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (timestamp == null) { timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet)); } - final String body = packet.getBody(); - final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user"); + final LocalizedContent body = packet.getBody(); + final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER); final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0"); final Element oob = packet.findChild("x", Namespace.OOB); @@ -337,7 +340,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final URL xP1S3url = xP1S3 == null ? null : P1S3UrlStreamHandler.of(xP1S3); final String oobUrl = oob != null ? oob.findChildContent("url") : null; final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id"); - final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); + final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); int status; final Jid counterpart; final Jid to = packet.getTo(); @@ -377,9 +380,16 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece selfAddressed = false; } - Invite invite = extractInvite(account, packet); - if (invite != null && invite.execute(account)) { - return; + final Invite invite = extractInvite(packet); + if (invite != null) { + if (isTypeGroupChat) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring invite to "+invite.jid+" because type=groupchat"); + } else if (invite.direct && (mucUserElement != null || invite.inviter == null || mXmppConnectionService.isMuc(account, invite.inviter))) { + Log.d(Config.LOGTAG, account.getJid().asBareJid()+": ignoring direct invite to "+invite.jid+" because it was received in MUC"); + } else { + invite.execute(account); + return; + } } if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) { @@ -409,10 +419,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId)) { return; } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) { - Message message = conversation.findSentMessageWithBody(packet.getBody()); - if (message != null) { - mXmppConnectionService.markMessage(message, status); - return; + LocalizedContent localizedBody = packet.getBody(); + if (localizedBody != null) { + Message message = conversation.findSentMessageWithBody(localizedBody.content); + if (message != null) { + mXmppConnectionService.markMessage(message, status); + return; + } } } } else { @@ -491,7 +504,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece message.setEncryption(Message.ENCRYPTION_DECRYPTED); } } else { - message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); + message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status); + if (body.count > 1) { + message.setBodyLanguage(body.language); + } } message.setCounterpart(counterpart); @@ -499,7 +515,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece message.setServerMsgId(serverMsgId); message.setCarbon(isCarbon); message.setTime(timestamp); - if (body != null && body.equals(oobUrl)) { + if (body != null && body.content != null && body.content.equals(oobUrl)) { message.setOob(true); if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) { message.setEncryption(Message.ENCRYPTION_DECRYPTED); @@ -702,11 +718,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } if (isTypeGroupChat) { - if (packet.hasChild("subject")) { + if (packet.hasChild("subject")) { //TODO usually we would want to check for lack of body; however some servers do set a body :( if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0); - String subject = packet.findInternationalizedChildContent("subject"); - if (conversation.getMucOptions().setSubject(subject)) { + final LocalizedContent subject = packet.findInternationalizedChildContentInDefaultNamespace("subject"); + if (subject != null && conversation.getMucOptions().setSubject(subject.content)) { mXmppConnectionService.updateConversation(conversation); } mXmppConnectionService.updateConversationUi(); @@ -791,9 +807,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece mXmppConnectionService.markRead(conversation); } } else if (!counterpart.isBareJid() && trueJid != null) { - ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid); + final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid); if (message.addReadByMarker(readByMarker)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added read by (" + readByMarker.getRealJid() + ") to message '" + message.getBody() + "'"); mXmppConnectionService.updateMessage(message, false); } } @@ -873,11 +888,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece private class Invite { final Jid jid; final String password; - final Contact inviter; + final boolean direct; + final Jid inviter; - Invite(Jid jid, String password, Contact inviter) { + Invite(Jid jid, String password, boolean direct, Jid inviter) { this.jid = jid; this.password = password; + this.direct = direct; this.inviter = inviter; } @@ -890,7 +907,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } else { conversation.getMucOptions().setPassword(password); mXmppConnectionService.databaseBackend.updateConversation(conversation); - mXmppConnectionService.joinMuc(conversation, inviter != null && inviter.mutualPresenceSubscription()); + final Contact contact = inviter != null ? account.getRoster().getContactFromContactList(inviter) : null; + mXmppConnectionService.joinMuc(conversation, contact != null && contact.mutualPresenceSubscription()); mXmppConnectionService.updateConversationUi(); } return true; diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 065c06023..9273df976 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -60,7 +60,7 @@ public class PresenceParser extends AbstractParser implements final Jid from = packet.getFrom(); if (!from.isBareJid()) { final String type = packet.getAttribute("type"); - final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user"); + final Element x = packet.findChild("x", Namespace.MUC_USER); Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); final List codes = getStatusCodes(x); if (type == null) { @@ -364,7 +364,7 @@ public class PresenceParser extends AbstractParser implements @Override public void onPresencePacketReceived(Account account, PresencePacket packet) { - if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + if (packet.hasChild("x", Namespace.MUC_USER)) { this.parseConferencePresence(packet, account); } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { this.parseConferencePresence(packet, account); diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index d90feec31..e847426ea 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -61,7 +61,7 @@ import rocks.xmpp.addr.Jid; public class DatabaseBackend extends SQLiteOpenHelper { private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 44; + private static final int DATABASE_VERSION = 45; private static DatabaseBackend instance = null; private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -225,6 +225,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Message.READ_BY_MARKERS + " TEXT," + Message.MARKABLE + " NUMBER DEFAULT 0," + Message.DELETED + " NUMBER DEFAULT 0," + + Message.BODY_LANGUAGE + " TEXT," + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.CONVERSATION + ") REFERENCES " + Conversation.TABLENAME + "(" + Conversation.UUID @@ -522,6 +523,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL(CREATE_MESSAGE_TYPE_INDEX); } + if (oldVersion < 45 && newVersion >= 45) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.BODY_LANGUAGE); + } + db.execSQL("DROP TABLE resolver_results"); } diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index 306f8e189..f5dd08485 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -1,11 +1,10 @@ package eu.siacs.conversations.services; +import android.support.annotation.NonNull; import android.util.Log; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; import java.io.IOException; import java.util.Collections; @@ -17,6 +16,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.http.services.MuclumbusService; import okhttp3.OkHttpClient; +import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -32,13 +32,14 @@ public class ChannelDiscoveryService { private final Cache> cache; - public ChannelDiscoveryService(XmppConnectionService service) { + ChannelDiscoveryService(XmppConnectionService service) { this.service = service; this.cache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build(); } - public void initializeMuclumbusService() { - OkHttpClient.Builder builder = new OkHttpClient.Builder(); + void initializeMuclumbusService() { + final OkHttpClient.Builder builder = new OkHttpClient.Builder(); + if (service.useTorToConnect()) { try { builder.proxy(HttpConnectionManager.getProxy()); @@ -55,9 +56,8 @@ public class ChannelDiscoveryService { this.muclumbusService = retrofit.create(MuclumbusService.class); } - public void discover(String query, OnChannelSearchResultsFound onChannelSearchResultsFound) { + void discover(String query, OnChannelSearchResultsFound onChannelSearchResultsFound) { final boolean all = query == null || query.trim().isEmpty(); - Log.d(Config.LOGTAG, "discover channels. query=" + query); List result = cache.getIfPresent(all ? "" : query); if (result != null) { onChannelSearchResultsFound.onChannelSearchResultsFound(result); @@ -75,9 +75,11 @@ public class ChannelDiscoveryService { try { call.enqueue(new Callback() { @Override - public void onResponse(Call call, Response response) { + public void onResponse(@NonNull Call call, @NonNull Response response) { final MuclumbusService.Rooms body = response.body(); if (body == null) { + listener.onChannelSearchResultsFound(Collections.emptyList()); + logError(response); return; } cache.put("", body.items); @@ -85,8 +87,8 @@ public class ChannelDiscoveryService { } @Override - public void onFailure(Call call, Throwable throwable) { - Log.d(Config.LOGTAG, "Unable to query muclumbus on "+Config.CHANNEL_DISCOVERY, throwable); + public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { + Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable); listener.onChannelSearchResultsFound(Collections.emptyList()); } }); @@ -96,14 +98,16 @@ public class ChannelDiscoveryService { } private void discoverChannels(final String query, OnChannelSearchResultsFound listener) { - Call searchResultCall = muclumbusService.search(new MuclumbusService.SearchRequest(query)); + MuclumbusService.SearchRequest searchRequest = new MuclumbusService.SearchRequest(query); + Call searchResultCall = muclumbusService.search(searchRequest); searchResultCall.enqueue(new Callback() { @Override - public void onResponse(Call call, Response response) { - System.out.println(response.message()); - MuclumbusService.SearchResult body = response.body(); + public void onResponse(@NonNull Call call, @NonNull Response response) { + final MuclumbusService.SearchResult body = response.body(); if (body == null) { + listener.onChannelSearchResultsFound(Collections.emptyList()); + logError(response); return; } cache.put(query, body.result.items); @@ -111,13 +115,26 @@ public class ChannelDiscoveryService { } @Override - public void onFailure(Call call, Throwable throwable) { - Log.d(Config.LOGTAG, "Unable to query muclumbus on "+Config.CHANNEL_DISCOVERY, throwable); + public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { + Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable); listener.onChannelSearchResultsFound(Collections.emptyList()); } }); } + private static void logError(final Response response) { + final ResponseBody errorBody = response.errorBody(); + Log.d(Config.LOGTAG, "code from muclumbus=" + response.code()); + if (errorBody == null) { + return; + } + try { + Log.d(Config.LOGTAG,"error body="+errorBody.string()); + } catch (IOException e) { + //ignored + } + } + public interface OnChannelSearchResultsFound { void onChannelSearchResultsFound(List results); } diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 7cc3f807a..f35c83f24 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -954,7 +954,7 @@ public class NotificationService { createTryAgainIntent()); mBuilder.setDeleteIntent(createDismissErrorIntent()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE); + mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE); mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp); } else { mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index d40a3ccd5..68db573a9 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1383,10 +1383,8 @@ public class XmppConnectionService extends Service { } } - final boolean inProgressJoin; - synchronized (account.inProgressConferenceJoins) { - inProgressJoin = conversation.getMode() == Conversational.MODE_MULTI && (account.inProgressConferenceJoins.contains(conversation) || account.pendingConferenceJoins.contains(conversation)); - } + final boolean inProgressJoin = isJoinInProgress(conversation); + if (account.isOnlineAndConnected() && !inProgressJoin) { switch (message.getEncryption()) { @@ -1516,6 +1514,23 @@ public class XmppConnectionService extends Service { } } + private boolean isJoinInProgress(final Conversation conversation) { + final Account account = conversation.getAccount(); + synchronized (account.inProgressConferenceJoins) { + if (conversation.getMode() == Conversational.MODE_MULTI) { + final boolean inProgress = account.inProgressConferenceJoins.contains(conversation); + final boolean pending = account.pendingConferenceJoins.contains(conversation); + final boolean inProgressJoin = inProgress || pending; + if (inProgressJoin) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": holding back message to group. inProgress="+inProgress+", pending="+pending); + } + return inProgressJoin; + } else { + return false; + } + } + } + private void sendUnsentMessages(final Conversation conversation) { conversation.findWaitingMessages(message -> resendMessage(message, true)); } @@ -2190,6 +2205,7 @@ public class XmppConnectionService extends Service { leaveMuc(conversation); } conversations.remove(conversation); + mNotificationService.clear(conversation); } } if (account.getXmppConnection() != null) { @@ -2204,7 +2220,7 @@ public class XmppConnectionService extends Service { this.accounts.remove(account); this.mRosterSyncTaskManager.clear(account); updateAccountUi(); - getNotificationService().updateErrorNotification(); + mNotificationService.updateErrorNotification(); syncEnabledAccountSetting(); toggleForegroundService(); } @@ -2550,6 +2566,9 @@ public class XmppConnectionService extends Service { final MucOptions mucOptions = conversation.getMucOptions(); if (mucOptions.nonanonymous() && !mucOptions.membersOnly() && !conversation.getBooleanAttribute("accept_non_anonymous", false)) { + synchronized (account.inProgressConferenceJoins) { + account.inProgressConferenceJoins.remove(conversation); + } mucOptions.setError(MucOptions.Error.NON_ANONYMOUS); updateConversationUi(); if (onConferenceJoined != null) { @@ -2943,9 +2962,11 @@ public class XmppConnectionService extends Service { for (Jid invite : jids) { invite(conversation, invite); } - if (account.countPresences() > 1) { - directInvite(conversation, account.getJid().asBareJid()); - } + for(String resource : account.getSelfContact().getPresences().toResourceArray()) { + Jid other = account.getJid().withResource(resource); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": sending direct invite to "+other); + directInvite(conversation, other); + } saveConversationAsBookmark(conversation, name); if (callback != null) { callback.success(conversation); @@ -2989,31 +3010,31 @@ public class XmppConnectionService extends Service { @Override public void onIqPacketReceived(Account account, IqPacket packet) { if (packet.getType() == IqPacket.TYPE.RESULT) { + final MucOptions mucOptions = conversation.getMucOptions(); + final Bookmark bookmark = conversation.getBookmark(); + final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName()); - final MucOptions mucOptions = conversation.getMucOptions(); - final Bookmark bookmark = conversation.getBookmark(); - final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName()); + if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(packet))) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid()); + updateConversation(conversation); + } - if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(packet))) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid()); - updateConversation(conversation); - } - - if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) { - if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) { - pushBookmarks(account); - } - } + if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) { + if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) { + pushBookmarks(account); + } + } - if (callback != null) { - callback.onConferenceConfigurationFetched(conversation); - } + if (callback != null) { + callback.onConferenceConfigurationFetched(conversation); + } - - updateConversationUi(); - } else if (packet.getType() == IqPacket.TYPE.ERROR) { + updateConversationUi(); + } else if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received timeout waiting for conference configuration fetch"); + } else { if (callback != null) { callback.onFetchFailed(conversation, packet.getError()); } @@ -3073,7 +3094,6 @@ public class XmppConnectionService extends Service { if (packet.getType() == IqPacket.TYPE.RESULT) { Data data = Data.parse(packet.query().findChild("x", Namespace.DATA)); data.submit(options); - Log.d(Config.LOGTAG,data.toString()); IqPacket set = new IqPacket(IqPacket.TYPE.SET); set.setTo(conversation.getJid().asBareJid()); set.query("http://jabber.org/protocol/muc#owner").addChild(data); diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java index ab5b66b10..10bd4ced1 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java @@ -2,8 +2,10 @@ package eu.siacs.conversations.ui; import android.app.AlertDialog; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.databinding.DataBindingUtil; +import android.net.Uri; import android.os.Bundle; import android.support.v7.widget.Toolbar; import android.text.Html; @@ -37,11 +39,8 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O private static final String CHANNEL_DISCOVERY_OPT_IN = "channel_discovery_opt_in"; private final ChannelSearchResultAdapter adapter = new ChannelSearchResultAdapter(); - - private ActivityChannelDiscoveryBinding binding; - private final PendingItem mInitialSearchValue = new PendingItem<>(); - + private ActivityChannelDiscoveryBinding binding; private MenuItem mMenuSearchView; private EditText mSearchEditText; @@ -198,6 +197,26 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O } + @Override + public boolean onContextItemSelected(MenuItem item) { + final MuclumbusService.Room room = adapter.getCurrent(); + if (room != null) { + switch (item.getItemId()) { + case R.id.share_with: + StartConversationActivity.shareAsChannel(this, room.address); + return true; + case R.id.open_join_dialog: + final Intent intent = new Intent(this, StartConversationActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra("force_dialog", true); + intent.setData(Uri.parse(String.format("xmpp:%s?join", room.address))); + startActivity(intent); + return true; + } + } + return false; + } + public void joinChannelSearchResult(String accountJid, MuclumbusService.Room result) { final boolean syncAutojoin = getBooleanPreference("autojoin", R.bool.autojoin); Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid)); diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index ee95ae7b6..086ab7101 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -13,6 +13,9 @@ import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Intents; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.RelativeSizeSpan; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -49,6 +52,7 @@ import eu.siacs.conversations.ui.util.JidDialog; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.Compatibility; +import eu.siacs.conversations.utils.Emoticons; import eu.siacs.conversations.utils.IrregularUnicodeDetector; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.XmppUri; @@ -328,14 +332,19 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp List statusMessages = contact.getPresences().getStatusMessages(); if (statusMessages.size() == 0) { binding.statusMessage.setVisibility(View.GONE); + } else if (statusMessages.size() == 1) { + final String message = statusMessages.get(0); + binding.statusMessage.setVisibility(View.VISIBLE); + final Spannable span = new SpannableString(message); + if (Emoticons.isOnlyEmoji(message)) { + span.setSpan(new RelativeSizeSpan(2.0f), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + binding.statusMessage.setText(span); } else { StringBuilder builder = new StringBuilder(); binding.statusMessage.setVisibility(View.VISIBLE); int s = statusMessages.size(); for (int i = 0; i < s; ++i) { - if (s > 1) { - builder.append("• "); - } builder.append(statusMessages.get(i)); if (i < s - 1) { builder.append("\n"); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 3b4842437..161e5bf42 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1552,7 +1552,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke intent = GeoHelper.getFetchIntent(activity); break; } - if (intent.resolveActivity(getActivity().getPackageManager()) != null) { + final Context context = getActivity(); + if (context != null && intent.resolveActivity(context.getPackageManager()) != null) { if (chooser) { startActivityForResult( Intent.createChooser(intent, getString(R.string.perform_action_with)), diff --git a/src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java b/src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java index b9c9b15b1..05dca588d 100644 --- a/src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java @@ -4,6 +4,7 @@ import android.app.Activity; import android.app.Dialog; import android.databinding.DataBindingUtil; import android.support.annotation.NonNull; +import android.support.design.widget.TextInputLayout; import android.support.v4.app.DialogFragment; import android.content.Context; import android.content.DialogInterface; @@ -65,9 +66,9 @@ public class JoinConferenceDialog extends DialogFragment implements OnBackendCon builder.setNegativeButton(R.string.cancel, null); AlertDialog dialog = builder.create(); dialog.show(); - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(view -> mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.jid, binding.bookmark.isChecked())); + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(view -> mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.accountJidLayout, binding.jid, binding.bookmark.isChecked())); binding.jid.setOnEditorActionListener((v, actionId, event) -> { - mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.jid, binding.bookmark.isChecked()); + mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.accountJidLayout, binding.jid, binding.bookmark.isChecked()); return true; }); return dialog; @@ -116,6 +117,6 @@ public class JoinConferenceDialog extends DialogFragment implements OnBackendCon } public interface JoinConferenceDialogListener { - void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, AutoCompleteTextView jid, boolean isBookmarkChecked); + void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, TextInputLayout jidLayout, AutoCompleteTextView jid, boolean isBookmarkChecked); } } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 4de40206c..9264d039e 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -15,6 +15,7 @@ import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.design.widget.TextInputLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; @@ -405,14 +406,18 @@ public class StartConversationActivity extends XmppActivity implements XmppConne protected void shareBookmarkUri(int position) { Bookmark bookmark = (Bookmark) conferences.get(position); + shareAsChannel(this, bookmark.getJid().asBareJid().toEscapedString()); + } + + public static void shareAsChannel(final Context context, final String address) { Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + bookmark.getJid().asBareJid().toEscapedString() + "?join"); + shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + address + "?join"); shareIntent.setType("text/plain"); try { - startActivity(Intent.createChooser(shareIntent, getText(R.string.share_uri_with))); + context.startActivity(Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with))); } catch (ActivityNotFoundException e) { - Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show(); + Toast.makeText(context, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show(); } } @@ -833,6 +838,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (uri != null) { Invite invite = new Invite(intent.getData(), intent.getBooleanExtra("scanned", false)); invite.account = intent.getStringExtra("account"); + invite.forceDialog = intent.getBooleanExtra("force_dialog", false); return invite.invite(); } else { return false; @@ -845,7 +851,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne List contacts = xmppConnectionService.findContacts(invite.getJid(), invite.account); if (invite.isAction(XmppUri.ACTION_JOIN)) { Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid()); - if (muc != null) { + if (muc != null && !invite.forceDialog) { switchToConversationDoNotAppend(muc, invite.getBody()); return true; } else { @@ -1000,7 +1006,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } @Override - public void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, AutoCompleteTextView jid, boolean isBookmarkChecked) { + public void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, TextInputLayout layout, AutoCompleteTextView jid, boolean isBookmarkChecked) { if (!xmppConnectionServiceBound) { return; } @@ -1008,17 +1014,26 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (account == null) { return; } - final Jid conferenceJid; + final String input = jid.getText().toString(); + Jid conferenceJid; try { - conferenceJid = Jid.of(jid.getText().toString()); + conferenceJid = Jid.of(input); } catch (final IllegalArgumentException e) { - jid.setError(getString(R.string.invalid_jid)); - return; + final XmppUri xmppUri = new XmppUri(input); + if (xmppUri.isJidValid() && xmppUri.isAction(XmppUri.ACTION_JOIN)) { + final Editable editable = jid.getEditableText(); + editable.clear(); + editable.append(xmppUri.getJid().toEscapedString()); + conferenceJid = xmppUri.getJid(); + } else { + layout.setError(getString(R.string.invalid_jid)); + return; + } } if (isBookmarkChecked) { if (account.hasBookmarkFor(conferenceJid)) { - jid.setError(getString(R.string.bookmark_already_exists)); + layout.setError(getString(R.string.bookmark_already_exists)); } else { final Bookmark bookmark = new Bookmark(account, conferenceJid.asBareJid()); bookmark.setAutojoin(getBooleanPreference("autojoin", R.bool.autojoin)); @@ -1274,6 +1289,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne public String account; + public boolean forceDialog = false; + public Invite(final Uri uri) { super(uri); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java index 03f09f70c..5ba28c446 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java @@ -1,11 +1,13 @@ package eu.siacs.conversations.ui.adapter; +import android.app.Activity; import android.databinding.DataBindingUtil; import android.support.annotation.NonNull; import android.support.v7.recyclerview.extensions.ListAdapter; import android.support.v7.util.DiffUtil; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; +import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -15,11 +17,11 @@ import java.util.Locale; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.SearchResultItemBinding; import eu.siacs.conversations.http.services.MuclumbusService; +import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.util.AvatarWorkerTask; +import rocks.xmpp.addr.Jid; -public class ChannelSearchResultAdapter extends ListAdapter { - - private OnChannelSearchResultSelected listener; +public class ChannelSearchResultAdapter extends ListAdapter implements View.OnCreateContextMenuListener { private static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() { @Override @@ -32,6 +34,8 @@ public class ChannelSearchResultAdapter extends ListAdapter listener.onChannelSearchResult(searchResult)); + final View root = viewHolder.binding.getRoot(); + root.setTag(searchResult); + root.setOnClickListener(v -> listener.onChannelSearchResult(searchResult)); + root.setOnCreateContextMenuListener(this); } public void setOnChannelSearchResultSelectedListener(OnChannelSearchResultSelected listener) { this.listener = listener; } + public MuclumbusService.Room getCurrent() { + return this.current; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + final Activity activity = XmppActivity.find(v); + final Object tag = v.getTag(); + if (activity != null && tag instanceof MuclumbusService.Room) { + activity.getMenuInflater().inflate(R.menu.channel_item_context, menu); + this.current = (MuclumbusService.Room) tag; + } + } + + + public interface OnChannelSearchResultSelected { + void onChannelSearchResult(MuclumbusService.Room result); + } public static class ViewHolder extends RecyclerView.ViewHolder { @@ -80,8 +106,4 @@ public class ChannelSearchResultAdapter extends ListAdapter implements CopyTextVie viewHolder.indicator.setVisibility(View.VISIBLE); } - String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); - if (message.getStatus() <= Message.STATUS_RECEIVED) { + final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); + final String bodyLanguage = message.getBodyLanguage(); + final String bodyLanguageInfo = bodyLanguage == null ? "" : String.format(" \u00B7 %s", bodyLanguage.toUpperCase(Locale.US)); + if (message.getStatus() <= Message.STATUS_RECEIVED) { ; if ((filesize != null) && (info != null)) { - viewHolder.time.setText(formatedTime + " \u00B7 " + filesize + " \u00B7 " + info); + viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + " \u00B7 " + info + bodyLanguageInfo); } else if ((filesize == null) && (info != null)) { - viewHolder.time.setText(formatedTime + " \u00B7 " + info); + viewHolder.time.setText(formattedTime + " \u00B7 " + info + bodyLanguageInfo); } else if ((filesize != null) && (info == null)) { - viewHolder.time.setText(formatedTime + " \u00B7 " + filesize); + viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + bodyLanguageInfo); } else { - viewHolder.time.setText(formatedTime); + viewHolder.time.setText(formattedTime+bodyLanguageInfo); } } else { if ((filesize != null) && (info != null)) { - viewHolder.time.setText(filesize + " \u00B7 " + info); + viewHolder.time.setText(filesize + " \u00B7 " + info + bodyLanguageInfo); } else if ((filesize == null) && (info != null)) { if (error) { - viewHolder.time.setText(info + " \u00B7 " + formatedTime); + viewHolder.time.setText(info + " \u00B7 " + formattedTime + bodyLanguageInfo); } else { viewHolder.time.setText(info); } } else if ((filesize != null) && (info == null)) { - viewHolder.time.setText(filesize + " \u00B7 " + formatedTime); + viewHolder.time.setText(filesize + " \u00B7 " + formattedTime + bodyLanguageInfo); } else { - viewHolder.time.setText(formatedTime); + viewHolder.time.setText(formattedTime+bodyLanguageInfo); } } } diff --git a/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java b/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java index 04198e781..c3e874310 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java @@ -147,6 +147,8 @@ public final class MucDetailsContextMenuHelper { activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.ADMIN, onAffiliationChanged); return true; case R.id.give_membership: + case R.id.remove_admin_privileges: + case R.id.revoke_owner_privileges: activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.MEMBER, onAffiliationChanged); return true; case R.id.give_owner_privileges: @@ -155,10 +157,6 @@ public final class MucDetailsContextMenuHelper { case R.id.remove_membership: activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.NONE, onAffiliationChanged); return true; - case R.id.remove_admin_privileges: - case R.id.revoke_owner_privileges: - activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.MEMBER, onAffiliationChanged); - return true; case R.id.remove_from_room: removeFromRoom(user, activity, onAffiliationChanged); return true; @@ -180,7 +178,7 @@ public final class MucDetailsContextMenuHelper { return true; case R.id.invite: if (user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) { - activity.xmppConnectionService.directInvite(conversation, jid); + activity.xmppConnectionService.directInvite(conversation, jid.asBareJid()); } else { activity.xmppConnectionService.invite(conversation, jid); } diff --git a/src/main/java/eu/siacs/conversations/ui/widget/CopyTextView.java b/src/main/java/eu/siacs/conversations/ui/widget/CopyTextView.java index bed56192e..d22a2deae 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/CopyTextView.java +++ b/src/main/java/eu/siacs/conversations/ui/widget/CopyTextView.java @@ -1,14 +1,12 @@ package eu.siacs.conversations.ui.widget; -import android.annotation.TargetApi; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.os.Build; +import android.support.v7.widget.AppCompatTextView; import android.util.AttributeSet; -import android.widget.TextView; -public class CopyTextView extends TextView { +public class CopyTextView extends AppCompatTextView { public CopyTextView(Context context) { super(context); @@ -22,14 +20,8 @@ public class CopyTextView extends TextView { super(context, attrs, defStyleAttr); } - @SuppressWarnings("unused") - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public CopyTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - public interface CopyHandler { - public String transformTextForCopy(CharSequence text, int start, int end); + String transformTextForCopy(CharSequence text, int start, int end); } private CopyHandler copyHandler; @@ -40,7 +32,7 @@ public class CopyTextView extends TextView { @Override public boolean onTextContextMenuItem(int id) { - CharSequence text = getText(); + final CharSequence text = getText(); int min = 0; int max = text.length(); if (isFocused()) { diff --git a/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java b/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java index 1c32fd75a..24f1cac8d 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java +++ b/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java @@ -1,8 +1,5 @@ package eu.siacs.conversations.ui.widget; -import java.lang.reflect.Field; -import java.lang.reflect.Method; - import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -13,199 +10,202 @@ import android.view.Menu; import android.view.MenuItem; import android.widget.TextView; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + public class ListSelectionManager { - private static final int MESSAGE_SEND_RESET = 1; - private static final int MESSAGE_RESET = 2; - private static final int MESSAGE_START_SELECTION = 3; + private static final int MESSAGE_SEND_RESET = 1; + private static final int MESSAGE_RESET = 2; + private static final int MESSAGE_START_SELECTION = 3; + private static final Field FIELD_EDITOR; + private static final Method METHOD_START_SELECTION; + private static final boolean SUPPORTED; + private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Handler.Callback() { - private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_SEND_RESET: { + // Skip one more message queue loop + HANDLER.obtainMessage(MESSAGE_RESET, msg.obj).sendToTarget(); + return true; + } + case MESSAGE_RESET: { + final ListSelectionManager listSelectionManager = (ListSelectionManager) msg.obj; + listSelectionManager.futureSelectionIdentifier = null; + return true; + } + case MESSAGE_START_SELECTION: { + final StartSelectionHolder holder = (StartSelectionHolder) msg.obj; + holder.listSelectionManager.futureSelectionIdentifier = null; + startSelection(holder.textView, holder.start, holder.end); + return true; + } + } + return false; + } + }); - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MESSAGE_SEND_RESET: { - // Skip one more message queue loop - HANDLER.obtainMessage(MESSAGE_RESET, msg.obj).sendToTarget(); - return true; - } - case MESSAGE_RESET: { - final ListSelectionManager listSelectionManager = (ListSelectionManager) msg.obj; - listSelectionManager.futureSelectionIdentifier = null; - return true; - } - case MESSAGE_START_SELECTION: { - final StartSelectionHolder holder = (StartSelectionHolder) msg.obj; - holder.listSelectionManager.futureSelectionIdentifier = null; - startSelection(holder.textView, holder.start, holder.end); - return true; - } - } - return false; - } - }); + static { + Field editor; + try { + editor = TextView.class.getDeclaredField("mEditor"); + editor.setAccessible(true); + } catch (Exception e) { + editor = null; + } + FIELD_EDITOR = editor; + Method startSelection = null; + if (editor != null) { + String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"}; + for (String startSelectionName : startSelectionNames) { + try { + startSelection = editor.getType().getDeclaredMethod(startSelectionName); + startSelection.setAccessible(true); + break; + } catch (Exception e) { + startSelection = null; + } + } + } + METHOD_START_SELECTION = startSelection; + SUPPORTED = FIELD_EDITOR != null && METHOD_START_SELECTION != null; + } - private static class StartSelectionHolder { + private ActionMode selectionActionMode; + private Object selectionIdentifier; + private TextView selectionTextView; + private Object futureSelectionIdentifier; + private int futureSelectionStart; + private int futureSelectionEnd; - public final ListSelectionManager listSelectionManager; - public final TextView textView; - public final int start; - public final int end; + public static boolean isSupported() { + return SUPPORTED; + } - public StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView, - int start, int end) { - this.listSelectionManager = listSelectionManager; - this.textView = textView; - this.start = start; - this.end = end; - } - } + private static void startSelection(TextView textView, int start, int end) { + final CharSequence text = textView.getText(); + if (SUPPORTED && start >= 0 && end > start && textView.isTextSelectable() && text instanceof Spannable) { + final Spannable spannable = (Spannable) text; + start = Math.min(start, spannable.length()); + end = Math.min(end, spannable.length()); + Selection.setSelection(spannable, start, end); + try { + final Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(textView) : textView; + METHOD_START_SELECTION.invoke(editor); + } catch (Exception e) { + } + } + } - private ActionMode selectionActionMode; - private Object selectionIdentifier; - private TextView selectionTextView; + public void onCreate(TextView textView, ActionMode.Callback additionalCallback) { + final CustomCallback callback = new CustomCallback(textView, additionalCallback); + textView.setCustomSelectionActionModeCallback(callback); + } - private Object futureSelectionIdentifier; - private int futureSelectionStart; - private int futureSelectionEnd; + public void onUpdate(TextView textView, Object identifier) { + if (SUPPORTED) { + final ActionMode.Callback callback = textView.getCustomSelectionActionModeCallback(); + if (callback instanceof CustomCallback) { + final CustomCallback customCallback = (CustomCallback) textView.getCustomSelectionActionModeCallback(); + customCallback.identifier = identifier; + if (futureSelectionIdentifier == identifier) { + HANDLER.obtainMessage(MESSAGE_START_SELECTION, new StartSelectionHolder(this, + textView, futureSelectionStart, futureSelectionEnd)).sendToTarget(); + } + } + } + } - public void onCreate(TextView textView, ActionMode.Callback additionalCallback) { - final CustomCallback callback = new CustomCallback(textView, additionalCallback); - textView.setCustomSelectionActionModeCallback(callback); - } + public void onBeforeNotifyDataSetChanged() { + if (SUPPORTED) { + HANDLER.removeMessages(MESSAGE_SEND_RESET); + HANDLER.removeMessages(MESSAGE_RESET); + HANDLER.removeMessages(MESSAGE_START_SELECTION); + if (selectionActionMode != null) { + final CharSequence text = selectionTextView.getText(); + futureSelectionIdentifier = selectionIdentifier; + futureSelectionStart = Selection.getSelectionStart(text); + futureSelectionEnd = Selection.getSelectionEnd(text); + selectionActionMode.finish(); + selectionActionMode = null; + selectionIdentifier = null; + selectionTextView = null; + } + } + } - public void onUpdate(TextView textView, Object identifier) { - if (SUPPORTED) { - CustomCallback callback = (CustomCallback) textView.getCustomSelectionActionModeCallback(); - callback.identifier = identifier; - if (futureSelectionIdentifier == identifier) { - HANDLER.obtainMessage(MESSAGE_START_SELECTION, new StartSelectionHolder(this, - textView, futureSelectionStart, futureSelectionEnd)).sendToTarget(); - } - } - } + public void onAfterNotifyDataSetChanged() { + if (SUPPORTED && futureSelectionIdentifier != null) { + HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget(); + } + } - public void onBeforeNotifyDataSetChanged() { - if (SUPPORTED) { - HANDLER.removeMessages(MESSAGE_SEND_RESET); - HANDLER.removeMessages(MESSAGE_RESET); - HANDLER.removeMessages(MESSAGE_START_SELECTION); - if (selectionActionMode != null) { - final CharSequence text = selectionTextView.getText(); - futureSelectionIdentifier = selectionIdentifier; - futureSelectionStart = Selection.getSelectionStart(text); - futureSelectionEnd = Selection.getSelectionEnd(text); - selectionActionMode.finish(); - selectionActionMode = null; - selectionIdentifier = null; - selectionTextView = null; - } - } - } + private static class StartSelectionHolder { - public void onAfterNotifyDataSetChanged() { - if (SUPPORTED && futureSelectionIdentifier != null) { - HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget(); - } - } + final ListSelectionManager listSelectionManager; + final TextView textView; + public final int start; + public final int end; - private class CustomCallback implements ActionMode.Callback { + StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView, + int start, int end) { + this.listSelectionManager = listSelectionManager; + this.textView = textView; + this.start = start; + this.end = end; + } + } - private final TextView textView; - private final ActionMode.Callback additionalCallback; - public Object identifier; + private class CustomCallback implements ActionMode.Callback { - public CustomCallback(TextView textView, ActionMode.Callback additionalCallback) { - this.textView = textView; - this.additionalCallback = additionalCallback; - } + private final TextView textView; + private final ActionMode.Callback additionalCallback; + Object identifier; - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - selectionActionMode = mode; - selectionIdentifier = identifier; - selectionTextView = textView; - if (additionalCallback != null) { - additionalCallback.onCreateActionMode(mode, menu); - } - return true; - } + CustomCallback(TextView textView, ActionMode.Callback additionalCallback) { + this.textView = textView; + this.additionalCallback = additionalCallback; + } - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - if (additionalCallback != null) { - additionalCallback.onPrepareActionMode(mode, menu); - } - return true; - } + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + selectionActionMode = mode; + selectionIdentifier = identifier; + selectionTextView = textView; + if (additionalCallback != null) { + additionalCallback.onCreateActionMode(mode, menu); + } + return true; + } - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) { - return true; - } - return false; - } + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + if (additionalCallback != null) { + additionalCallback.onPrepareActionMode(mode, menu); + } + return true; + } - @Override - public void onDestroyActionMode(ActionMode mode) { - if (additionalCallback != null) { - additionalCallback.onDestroyActionMode(mode); - } - if (selectionActionMode == mode) { - selectionActionMode = null; - selectionIdentifier = null; - selectionTextView = null; - } - } - } + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) { + return true; + } + return false; + } - private static final Field FIELD_EDITOR; - private static final Method METHOD_START_SELECTION; - private static final boolean SUPPORTED; - - static { - Field editor; - try { - editor = TextView.class.getDeclaredField("mEditor"); - editor.setAccessible(true); - } catch (Exception e) { - editor = null; - } - FIELD_EDITOR = editor; - Method startSelection = null; - if (editor != null) { - String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"}; - for (String startSelectionName : startSelectionNames) { - try { - startSelection = editor.getType().getDeclaredMethod(startSelectionName); - startSelection.setAccessible(true); - break; - } catch (Exception e) { - startSelection = null; - } - } - } - METHOD_START_SELECTION = startSelection; - SUPPORTED = FIELD_EDITOR != null && METHOD_START_SELECTION != null; - } - - public static boolean isSupported() { - return SUPPORTED; - } - - public static void startSelection(TextView textView, int start, int end) { - final CharSequence text = textView.getText(); - if (SUPPORTED && start >= 0 && end > start && textView.isTextSelectable() && text instanceof Spannable) { - final Spannable spannable = (Spannable) text; - start = Math.min(start, spannable.length()); - end = Math.min(end, spannable.length()); - Selection.setSelection(spannable, start, end); - try { - final Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(textView) : textView; - METHOD_START_SELECTION.invoke(editor); - } catch (Exception e) { - } - } - } + @Override + public void onDestroyActionMode(ActionMode mode) { + if (additionalCallback != null) { + additionalCallback.onDestroyActionMode(mode); + } + if (selectionActionMode == mode) { + selectionActionMode = null; + selectionIdentifier = null; + selectionTextView = null; + } + } + } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java b/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java index 42329c41a..b2ef794c8 100644 --- a/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java +++ b/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. + * Copyright (c) 2018-2019, Daniel Gultsch All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: @@ -40,6 +40,8 @@ import android.text.style.ForegroundColorSpan; import android.util.LruCache; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -57,6 +59,7 @@ public class IrregularUnicodeDetector { private static final Map NORMALIZATION_MAP; private static final LruCache CACHE = new LruCache<>(4096); + private static final List AMBIGUOUS_CYRILLIC = Arrays.asList("а","г","е","ѕ","і","q","о","р","с","у"); static { Map temp = new HashMap<>(); @@ -185,13 +188,41 @@ public class IrregularUnicodeDetector { private static Set findIrregularCodePoints(String word) { Set codePoints; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - codePoints = eliminateFirstAndGetCodePointsCompat(mapCompat(word)); + final Map> map = mapCompat(word); + final Set set = asSet(map); + if (containsOnlyAmbiguousCyrillic(set)) { + return set; + } + codePoints = eliminateFirstAndGetCodePointsCompat(map); } else { - codePoints = eliminateFirstAndGetCodePoints(map(word)); + final Map> map = map(word); + final Set set = asSet(map); + if (containsOnlyAmbiguousCyrillic(set)) { + return set; + } + codePoints = eliminateFirstAndGetCodePoints(map); } return codePoints; } + private static Set asSet(Map> map) { + final Set flat = new HashSet<>(); + for(List value : map.values()) { + flat.addAll(value); + } + return flat; + } + + + private static boolean containsOnlyAmbiguousCyrillic(Collection codePoints) { + for (String codePoint : codePoints) { + if (!AMBIGUOUS_CYRILLIC.contains(codePoint)) { + return false; + } + } + return true; + } + private static PatternTuple find(Jid jid) { synchronized (CACHE) { PatternTuple pattern = CACHE.get(jid); diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index c708ca2b1..2efd943de 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -1,15 +1,11 @@ package eu.siacs.conversations.xml; -import android.support.annotation.NonNull; -import android.util.Log; - import java.util.ArrayList; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Locale; -import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.XmlHelper; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; @@ -71,31 +67,8 @@ public class Element { return element == null ? null : element.getContent(); } - public String findInternationalizedChildContent(String name) { - return findInternationalizedChildContent(name, Locale.getDefault().getLanguage()); - } - - private String findInternationalizedChildContent(String name, @NonNull String language) { - final HashMap contents = new HashMap<>(); - for(Element child : this.children) { - if (name.equals(child.getName())) { - String lang = child.getAttribute("xml:lang"); - String content = child.getContent(); - if (content != null) { - if (language.equals(lang)) { - return content; - } else { - contents.put(lang, content); - } - } - } - } - final String value = contents.get(null); - if (value != null) { - return value; - } - final String[] values = contents.values().toArray(new String[0]); - return values.length == 0 ? null : values[0]; + public LocalizedContent findInternationalizedChildContentInDefaultNamespace(String name) { + return LocalizedContent.get(this, name); } public Element findChild(String name, String xmlns) { @@ -107,6 +80,19 @@ public class Element { return null; } + public Element findChildEnsureSingle(String name, String xmlns) { + final List results = new ArrayList<>(); + for (Element child : this.children) { + if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) { + results.add(child); + } + } + if (results.size() == 1) { + return results.get(0); + } + return null; + } + public String findChildContent(String name, String xmlns) { Element element = findChild(name,xmlns); return element == null ? null : element.getContent(); diff --git a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java new file mode 100644 index 000000000..57a2f3dba --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java @@ -0,0 +1,59 @@ +package eu.siacs.conversations.xml; + +import com.google.common.collect.Iterables; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public class LocalizedContent { + + public static final String STREAM_LANGUAGE = "en"; + + public final String content; + public final String language; + public final int count; + + private LocalizedContent(String content, String language, int count) { + this.content = content; + this.language = language; + this.count = count; + } + + public static LocalizedContent get(final Element element, String name) { + final HashMap contents = new HashMap<>(); + final String parentLanguage = element.getAttribute("xml:lang"); + for(Element child : element.children) { + if (name.equals(child.getName())) { + final String namespace = child.getNamespace(); + final String childLanguage = child.getAttribute("xml:lang"); + final String lang = childLanguage == null ? parentLanguage : childLanguage; + final String content = child.getContent(); + if (content != null && (namespace == null || "jabber:client".equals(namespace))) { + if (contents.put(lang, content) != null) { + //anything that has multiple contents for the same language is invalid + return null; + } + } + } + } + if (contents.size() == 0) { + return null; + } + final String userLanguage = Locale.getDefault().getLanguage(); + final String localized = contents.get(userLanguage); + if (localized != null) { + return new LocalizedContent(localized, userLanguage, contents.size()); + } + final String defaultLanguageContent = contents.get(null); + if (defaultLanguageContent != null) { + return new LocalizedContent(defaultLanguageContent, STREAM_LANGUAGE, contents.size()); + } + final String streamLanguageContent = contents.get(STREAM_LANGUAGE); + if (streamLanguageContent != null) { + return new LocalizedContent(streamLanguageContent, STREAM_LANGUAGE, contents.size()); + } + final Map.Entry first = Iterables.get(contents.entrySet(), 0); + return new LocalizedContent(first.getValue(), first.getKey(), contents.size()); + } +} diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 55d54853e..26a0b33a2 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -32,4 +32,5 @@ public final class Namespace { public static final String COMMANDS = "http://jabber.org/protocol/commands"; public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0"; public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0"; + public static final String MUC_USER = "http://jabber.org/protocol/muc#user"; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 702a8c6ca..6ce01194f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -80,6 +80,7 @@ import eu.siacs.conversations.utils.SSLSocketHelper; import eu.siacs.conversations.utils.SocksSocketFactory; import eu.siacs.conversations.utils.XmlHelper; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.LocalizedContent; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.TagWriter; @@ -1313,7 +1314,7 @@ public class XmppConnection implements Runnable { final Tag stream = Tag.start("stream:stream"); stream.setAttribute("to", account.getServer()); stream.setAttribute("version", "1.0"); - stream.setAttribute("xml:lang", "en"); + stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE); stream.setAttribute("xmlns", "jabber:client"); stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams"); tagWriter.writeTag(stream); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 41aa75ad9..be72d9327 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -142,7 +142,7 @@ public class JingleSocks5Transport extends JingleTransport { this.isEstablished = true; FileBackend.close(serverSocket); } else { - this.socket.close(); + FileBackend.close(socket); } } else { socket.close(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java index ac75a5e59..86068bf77 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -4,6 +4,7 @@ import android.util.Pair; import eu.siacs.conversations.parser.AbstractParser; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.LocalizedContent; public class MessagePacket extends AbstractAcknowledgeableStanza { public static final int TYPE_CHAT = 0; @@ -16,8 +17,8 @@ public class MessagePacket extends AbstractAcknowledgeableStanza { super("message"); } - public String getBody() { - return findChildContent("body"); + public LocalizedContent getBody() { + return findInternationalizedChildContentInDefaultNamespace("body"); } public void setBody(String text) { diff --git a/src/main/res/menu/channel_item_context.xml b/src/main/res/menu/channel_item_context.xml new file mode 100644 index 000000000..a274053e5 --- /dev/null +++ b/src/main/res/menu/channel_item_context.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index 63960d682..a6f32014f 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -7,6 +7,7 @@ أغلق هذه المحادثة بيانات جهة الإتصال تفاصيل مجموعة المحادثة + تفاصيل القناة تشفير المحادثة إضافة حساب تعديل الإسم @@ -16,6 +17,8 @@ إنهاء حجب جهة اتصال حجب دومين إنهاء حجب دومين + احجب المشارِك + إلغاء حجب المشارِك إدارة الحسابات إعدادات مشاركة مع محادثة @@ -50,6 +53,7 @@ مشاركة مع إبداء المحادثة دعوة جهة إتصال + دعوة جهات الإتصال جهة إتصال الغاء @@ -143,6 +147,8 @@ فشلت عملية التفاوض عبر TLS خرق للقواعد لا يتوافق مع السيرفر + خطأ في التدفق + خطأ عند فتح التدفق غير مشفر رسالة مشفرة عبر OTR رسالة مشفرة عبر OpenPGP @@ -157,6 +163,8 @@ هل أنت متأكد ؟ إذا قمت حذفت حسابك، فسوف تفقد سجل محادثاتك بالكامل تسجيل صوت + عنوان XMPP + احجب عنوان XMPP username@example.com كلمة السر خارج الذاكرة. الصورة كبيرة جدا @@ -186,11 +194,13 @@ بصمة OMEMO بصمة v\\OMEMO بصمة OMEMO للرسالة + بصمة v\\OMEMO للرسالة أجهزة أخرى الثقة في بصمات أوميمو OMEMO جارإحضار المفاتيح ... تم فك الشيفرة + الفواصل المرجعية بحث قم بإدخال جهة إتصال حذف جهة الإتصال @@ -200,11 +210,18 @@ أضف إختر جهة الاتصال موجودة لديك مسبقا - دخول + التحق + channel@conference.example.com/nick + channel@conference.example.com حفظ بالمفضلة إحذف من المفضلة + دمر فريق المحادثة + دمر القناة + لم نتمكن مِن تدمير فريق المحادثة + لم نتمكن مِن تدمير القناة موجوده بالمفضلة سابقا تعديل موضوع مجموعة المحادثة + الموضوع في صدد الإنظمام إلى مجموعة المحادثة ... غادر جهة اتصال أضافتك @@ -237,6 +254,7 @@ السماح لمراسليك بتعديل رسائلهم إعدادات متقدمة كن حذراً مع هذه من فضلك + عن %s ساعات السكون وقت البداية وقت النهاية @@ -244,6 +262,7 @@ سوف تكتم التنبيهات إبان ساعات السكون طلب تقارير تسليم الرسائل أخرى + زامِن مع الفواصل المرجعية تم نسخ بصمة OMEMO إلى الحافظة ! حسابك محظور للإلتحاق بمجموعة المحادثة هذه هذه المجموعة متاحة للأعضاء المنتمين إليها فقط @@ -253,11 +272,16 @@ أنت تستعمل حساب %s انقطع الإتصال .. حاول مرة أخرى تحقق من حجم %s + تحقق مِن حجم %1$s على %2$s خيارات الرسالة إقتبس + ألصقه كاقتباس أنسخ الرابط الأصلي أعد الإرسال رابط الملف + تم نسخ عنوان الـ XMPP إلى الحافظة + تم نسخ رسالة الخطأ إلى الحافظة + عنوان الويب إمسح شفرة التّعرّف 2D أظهر شفرة التّعرّف 2D إعرض قائمة المحبوسين @@ -266,6 +290,10 @@ حاول مرة أخرى احتفظ بالتطبيق يعمل في المقدمة منع نظام التشغيل من انهاء اتصالك + أنشئ نسخة احتياطية + تم إنشاء نسختك الاحتياطية + تم استرجاع نسختك الاحتياطية + لا تنسى تنشيط الحساب. اختيار ملف اكتمل الإستلام %1$s (%2$d%% بنسبة) تنزيل %s @@ -280,8 +308,10 @@ تم حذف الملف لا يوجد تطبيق متاح لعرض الملف تعذر العثور على تطبيق يمكنه فتح الرابط + وسوم ديناميكية عرض علامات للقراءة فقط أسفل بيانات جهات الإتصال تفعيل الإشعارات + لم يُعثر على أي خادم للمحادثات الجماعية فشلت عملية إنشاء مجموعة المحادثة ! الصورة الرمزية للحساب انسخ بصمة OMEMO إلى الحافظة @@ -308,11 +338,14 @@ منح امتيازات الإداره إلغاء امتيازات الإدارة التنحية من مجموعة المحادثة + أزله مِن القناة لا يمكن تغيير انتساب %s الحظر من دخول مجموعة المحادثة حظر الآن لا يمكن تغيير دول %s + إعدادات القناة العمومية سرِّي ، للأعضاء فقط + اجعل القناة تحت الإشراف لست مشتركا في المجموعة تم تعديل خيارات فريق المحادثة ! تعذر تغيير خيارات فريق المحادثة @@ -359,6 +392,7 @@ التي تم استعمالها كثيرا مؤخرا إختر حركة سريعة البحث في جهات الإتصال + البحث في الفواصل المرجعية إبعث برسالة على الخاص لقد غادَر %1$s فريق المحادثة ! إسم المستخدم @@ -413,6 +447,7 @@ تعطيل الإخطارات الإشعارات موقفة ضغط الصورة + تغيير حجم الصور وضغطها دائماً آليا وضع تحسين أداء البطارية مفعّل @@ -426,6 +461,8 @@ خطأ في الأمان : نفاذ غير سليم إلى ملف تعذر العثور على تطبيق يُمكنُ بواسطته مشاركة الرابط شارك الرابط مع ... + وافق ثم واصل + عنوان XMPP الخاص بك سيكون: %s إنشاء حساب إستخدم مزودي الخاص إختر إسم المستخدم @@ -442,6 +479,7 @@ إختر المشاركين جارٍ إنشاء مجموعة المحادثة ... أعد إرسال الدعوة + تعطيل قصير متوسط طويل @@ -556,9 +594,11 @@ لا يمكن تسجيل حسابات على هذا الخادوم إلا عبر موقع الويب فتح موقع الإنترنت تعذر العثور على تطبيق يُمكنه فتح موقع الويب + أظهر الإشعارات العلوية اليوم البارحة التحقق من صحة إسم المضيف بواسطة DNSSEC + الشهادة لا تحتوي على عنوان XMPP جُزْئِيًّا تسجيل فيديو النسخ إلى الحافظة @@ -572,11 +612,18 @@ تفاصيل الشهادة : مرة واحدة السحب إلى أسفل + التمرير إلى أسفل بعد إرسال رسالة تعديل رسالة حالة الحضور تعديل رسالة حالة الحضور تعطيل التعمية تعذر جلب قائمة الأجهزة تعطيله حالًا + المسودة: + التعمية بـ OMEMO + سوف يُستخدَم OMEMO افتراضيا في المحادثات الجديدة. + حجم الخط + نشِط مبدئيًا + معطل مبدئيًا صغير متوسط كبير @@ -588,16 +635,28 @@ مشاركة الموقع إظهار الموقع مشاركة + يرجى الانتظار… + البحث عن رسائل مشاهدة المحادثة نسخ العنوان الإلكتروني + انسخ عنوان الـ XMPP بحث مباشر إسم جهة الإتصال إسم مستعار إسم + إدخال الاسم اختياري + اسم فريق المحادثة + لا يمكن حفظ التسجيل + الخدمة الأمامية + معلومات عن الحالة مشاكل إتّصال رسائل رسائل + إعدادات الإشعار ضغط الفيديو + اعرض الوسائط + اعرض المشارِكين + المشارِكون جودة الفيديو متوسط (360ب) عالي (720ب) @@ -605,10 +664,22 @@ إختار الدولة رقم هاتف تحقق من رقم هاتفك + ليس %s برقم هاتف صالح. + يرجى إدخال رقم هاتفك. + البحث عن الدول + تحقق مِن %s إعادة إرسال الإرسالية القصيرة + يرجى الانتظار (%s) رجوع نعم لا + جارٍ التحقق… + جارٍ طلب الرسالة النصية القصيرة… + خطأ شبكي مجهول. + تعذر الربط بالخادم. + ليس هناك اتصال بالشبكة. + يرجى إعادة المحاولة في غضون %s + تحديث إسمك أدخل إسمك إضغط على زرّ التعديل لضبط إسمك @@ -620,4 +691,31 @@ إفتح بـ... صورة حساب كونفرسايشنز إختيار الحساب + استرجِع نسخة احتياطية + استرجِع + ادخِل عنوان XMPP + أنشئ فريق محادثة + إلتحِق بقناة عمومية + أنشئ فريق محادثة خاص + أنشئ قناة عمومية + اسم القناة + عنوان XMPP + يرجى إدخال اسمٍ للقناة + جارٍ إنشاء القناة العمومية… + لقد التحقت بقناة موجودة سابقا + اسمح لأي كان دعوة الآخرين + يمكن للمالِكين تعديل الموضوع. + إدارة الصلاحيات + البحث عن مشارِكين + حجم الملف كبير جدًا + أرفِق + استكشاف القنوات + البحث عن قنوات + لدي حساب + إضافة حساب موجود + تسجيل حساب جديد + أضفه على أي حال + حَدَث + افتح النسخة الاحتياطية + يرجى إدخال الكلمة السرية للحساب diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index dae6138a0..895707338 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -872,4 +872,5 @@ Dieses Konto wurde bereits eingerichtet Bitte gib das Passwort für dieses Konto ein Diese Aktion kann nicht ausgeführt werden + Öffentlichen Channel beitreten... diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index a5b0fb18d..3c63859d9 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -30,6 +30,7 @@ ahora hace 1 min hace %d min + %dconversaciones sin leer enviando… Descifrando mensaje. Por favor, espera... Mensaje cifrado con OpenPGP @@ -870,4 +871,5 @@ El fichero seleccionado no es un respaldo de Conversations Esta cuenta ya fue configurada Por favor ingrese la contraseña para esta cuenta + No se ha podido realizar esta acción diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index d0471a226..d9b407b0f 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -872,4 +872,5 @@ Esta conta xa foi configurada Introduza o contrasinal de esta conta Non se puido completar a acción + Unirse a canle pública... diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index e001699d7..2db97386c 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -30,6 +30,7 @@ adesso 1 min fa %d min fa + %d conversazioni non lette invio… Decifrazione messaggio. Attendere prego... Messaggio cifrato con OpenPGP diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index f39346363..42572df40 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -30,6 +30,7 @@ zojuist 1 min. geleden %d min. geleden + %d ongelezen gesprekken versturen… Bericht aan het ontsleutelen. Even geduld… OpenPGP-versleuteld bericht diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 253161494..580a01817 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -30,6 +30,7 @@ przed chwilą minutę temu %d minut temu + %d nieprzeczytanych konwersacji wysyłanie... Odszyfrowywanie wiadomości. To zajmie tylko chwilę... Wiadomość zaszyfrowana OpenPGP @@ -884,8 +885,9 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Kopia zapasowa Conversations Zdarzenie Otwórz kopię zapasową - Plik który otworzyłeś nie jest plikiem kopii zapasowej Conversations + Plik, który otworzyłeś, nie jest plikiem kopii zapasowej Conversations To konto zostało już ustawione Proszę podać hasło dla tego konta Nie można wykonać tej akcji + Dołącz do publicznego kanału... diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 5f1159cbb..f341c656e 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -30,6 +30,7 @@ în acest moment acum un minut acum %d minute + %d conversații necitite trimitere... Decriptez mesaj. Te rog așteaptă... Mesaj criptat cu OpenPGP @@ -412,7 +413,7 @@ Trimit %s Ofer %s Ascunde deconectat - %s tasteaza... + %s tastează... %s s-a oprit din scris %s tastează... %s s-au oprit din scris @@ -880,4 +881,5 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați Acest cont a fost deja configurat Va rugăm să introduceți parola pentru acest cont Nu se poate realiza această acțiune + Alătură-te unui canal public... diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 312d1f180..c7120852d 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -3,7 +3,10 @@ Inställningar Ny konversation Kontoinställningar + Stäng denna konversation Kontaktdetaljer + Gruppchattdetaljer + Kanaldetaljer Säker konversation Lägg till konto Ändra namn @@ -13,18 +16,25 @@ Avblockera kontakt Blockera domän Avblockera domän + Blockera deltagare + Avblockera deltagare Hantera konton Inställningar Dela med konversation Starta konversation + Välj kontakt + Välj kontakter + Dela via konto Blockeringslista just nu 1 min sedan %d min sedan + %d olästa konversationer skickar… Avkrypterar meddelande. Vänta… OpenPGP-krypterat meddelande Nick används redan + Ogiltigt nick Admin Ägare Moderator @@ -36,11 +46,16 @@ Blockera alla kontakter från %s? Avblockera alla kontakter från %s? Kontakt blockerad + Blockerad Vill du ta bort %s som bokmärke? Konversationer associerade med detta bokmärke kommer inte tas bort. Registrera nytt konto på servern Byt lösenord på server Dela med… + Börja konversation + Bjud in kontakt + Bjud in Kontakter + Kontakt Avbryt Sätt Lägg till @@ -66,6 +81,9 @@ Delar filer. Vänta... Rensa historik Rensa konversationshistorik + Är du säker på att du vill ta bort alla meddelanden i denna konversation?\n\nVarning: Detta kommer inte att ta bort kopior av dessa meddelanden på andra enheter eller servrar. + Ta bort fil + Stäng denna konversation efteråt Välj enhet Skicka okrypterat meddelande Skicka meddelande @@ -106,8 +124,10 @@ Bekräfta meddelanden Låt dina kontakter veta när du har mottagit och läst deras meddelanden Gränssnitt + Dålig krypterings-nyckel. Acceptera Ett fel har inträffat + Fel Ditt konto Skicka tillgänglighetsuppdatering Ta emot tillgänglighetsuppdateringar @@ -150,8 +170,11 @@ Är du säker? Om du tar bort ditt konto kommer hela konversationshistoriken att försvinna Spela in röst + XMPP-adress + Blockera XMPP-adress användarnamn@exempel.se Lösenord + Detta är inte en giltig XMPP-adress Slut på minne. Bilden är för stor Vill du lägga till %s i din enhets kontakter? Server-info @@ -186,8 +209,10 @@ Hämtar nycklar... Klar Avkryptera + Bokmärken Sök Fyll i kontakt + Ta bort kontakt Se kontaktdetaljer Blockera kontakt Avblockera kontakt @@ -198,10 +223,13 @@ Spara som bokmärke Ta bort bokmärke Detta bokmärke finns redan + Ämne + Går med i gruppchatt... Lämna Kontakten lade till dig i sin kontaktlista Addera tillbaka %s har läst hit + Alla har läst fram till hit Publicera Tryck på avatarbild för att välja en bild från bildgalleriet Publicerar… @@ -220,6 +248,7 @@ Hoppa över Inaktivera notifieringar Aktivera + Gruppchatten kräver lösenord Fyll i lösenord Begär tillgänglighetsuppdateringar från din kontakt först.\n\nDetta används för att se vilken klient/klienter din kontakt använder. Begär nu @@ -230,6 +259,7 @@ Tillåt att dina kontakter kan ändra sina meddelanden i efterhand Expertinställningar Var försiktig med dem + Om %s Tysta timmar Starttid Sluttid @@ -240,7 +270,10 @@ Mottagna meddelanden markeras med en grön bock om det stöds Färglägg skickaknappen för att indikera kontaktens status Annat + Synkronisera med bokmärken OMEMO-fingeravtryck har kopierats till urklipp! + Resursbegränsning + Gruppchatten stängdes ner använder konto %s Kontrollerar %s på webbserver Du är inte ansluten. Försök igen senare @@ -248,9 +281,14 @@ Kontrollera filstorlek för %1$s på %2$s Meddelandealternativ Citera + Klistra in som citat Kopiera orginal-URL Skicka igen Fil-URL + Kopierade URL till urklipp + Kopierade XMPP-adress till urklipp + Kopierade felmeddelande till urklipp + webbadress Scanna 2D-streckkod Visa 2D-streckkod Visa blockeringslista @@ -259,6 +297,14 @@ Försök igen Håll tjänst i förgrunden Förehindrar operativsystemet att ta ner uppkopplingen + Skapa säkerhetskopia + Säkerhetskopians filer lagras i %s + Skapar filer för säkerhetskopia + Din säkerhetskopia har skapats + Säkerhetskopians filer har lagrats i %s + Återställer säkerhetskopia + Din säkerhetskopia har återställts + Glöm inte att aktivera kontot. Välj fil Tar emot %1$s (%2$d%% klart) Ladda ner %s @@ -272,8 +318,11 @@ filöverföring lyckades inte Filen har blivit borttagen Ingen applikation kunde hittas för att öppna filen + Ingen applikation kunde hittas för att öppna länken + Ingen applikation kunde hittas för att visa kontakten Visa skrivskyddade taggar under kontakter Aktivera notifieringar + Misslyckades skapa gruppchatt! Kontots avatarbild Kopiera OMEMO-fingeravtryck till urklipp Regenerera OMEMO-nyckel @@ -298,6 +347,8 @@ Avancerat läge Bevilja administratörsbehörighet Återkalla administratörsbehörighet + Ta bort från gruppchatt + Ta bort från kanal Kunde inte ändra tillhörigheten för %s Bannlys nu Kunde inte ändra rollen för %s @@ -349,6 +400,7 @@ Senast använd Välj snabbfunktion Skicka privat meddelande + %1$s har lämnat gruppchatten! Användarnamn Användarnamn Inte ett giltigt användanamn @@ -360,8 +412,10 @@ Bind-fel Servern är inte ansvarig för domänen Sönder + Tillgänglighet Status borta när skärmen är av Sätter din tillgänglighet till borta när skrämen är av + \"Stör ej\" i tyst läge Hantera vibrationsläge som tyst läge Utökade anslutningsinställningar Visa val av servernamn och port vid inställning av konto @@ -398,10 +452,14 @@ Delade bild med %s Delade bilder med %s Delade text med %s + Conversations behöver tillgång till extern lagring + Conversations behöver tillgång till kameran Synkronisera med kontakter Notifiera för alla meddelanden Notifieringar deaktiverade Notifieringar pausade + Bildkomprimering + Ändra storlek på och komprimera bilder Alltid Automatiskt Batterioptimeringar aktiverade @@ -417,6 +475,8 @@ Säkerhetsfel: Ogiltig filaccess Ingen applikation kunde hittas för att dela URI Dela URI med... + Godkänn & fortsätt + Din fullständiga XMPP-adress kommer att vara: %s Skapa konto Använd min egen leverantör Välj användarnamn @@ -431,6 +491,7 @@ Registreringfel: Försök igen senare Registreringsfel: Lösenordet är för svagt Välj deltagare + Skapar gruppchatt... Bjud in igen Kort Medium @@ -492,10 +553,35 @@ Verifiera OMEMO-nycklar Lita ej på enhet Är du säker på att du vill ta bort verifieringen av denna enhet?\nDenna enhet och meddelanden som kommer från enheten kommer att markeras som ej pålitliga. + + %d sekund + %d sekunder + + + %d minut + %d minuter + + + %d timme + %d timmar + + + %d dag + %d dagar + + + %d vecka + %d veckor + + + %d månad + %d månader + Automatisk borttagning av meddelanden Ta automatiskt bort meddelanden från denna enhet som är äldre än den konfigurerade tidsramen. Krypterar meddelande Hämtar inte meddelanden på grund av inställningen för borttagning av gamla meddelanden. + Komprimerar video Motsvarande konversationer är stängda. Kontakt blockerad. Notifieringar från främlingar @@ -506,4 +592,29 @@ online just nu Försök dekryptera igen Sessionsfel + Idag + Igår + Certifikatet innehåller ej en XMPP-adress + Spela in video + Meddelande + Godkänn okänt certifikat? + Utkast: + Dela plats + Visa plats + Dela + Kopiera XMPP-adress + Gruppchattens namn + Deltagare + Välj ett land + telefonnummer + Bekräfta ditt telefonnummer + Skapa gruppchatt + Skapa sluten gruppchatt + Kanalnamn + Vänligen ange ett namn på kanalen + Denna kanal finns redan + Du har gått med i en befintlig kanal + Denna slutna gruppchatt har inga deltagare. + Upptäck kanaler + Detta ser ut som en kanaladress diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 3b4fa89f0..9995d90c4 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -1,33 +1,36 @@ 设置 - 新会话 + 新聊天 管理账户 管理账户 - 关闭对话 + 关闭聊天 联系人详情 群聊详情 频道详情 - 安全对话 + 安全聊天 添加账号 编辑姓名 - 添加到地址薄 - 从列表中删除 - 屏蔽联系人 - 解除联系人屏蔽 - 屏蔽域名 - 解除域名屏蔽 + 添加到联系人 + 从XMPP联系人中删除 + 封禁联系人 + 解封联系人 + 封禁域名 + 解封域名 + 封禁成员 + 解封成员 管理账户 设置 - 分享会话 - 开始会话 + 通过Conversations分享 + 开始聊天 选择联系人 选择联系人 通过帐户分享 - 屏蔽列表 + 封禁列表 刚刚 1分钟前 %d分钟前 + %d条未读消息 正在发送… 解密信息中. 请稍候… OpenPGP 加密的信息 @@ -35,19 +38,19 @@ 无效的用户名 管理员 所有者 - 群主 + 版主 参与者 访客 - 将 %s 从列表中移除? 与该联系人的会话消息不会清除. - 您想阻止%s向您发送消息吗? - 你想解除对 %s 的屏蔽吗,他们将可以发送信息给你? - 屏蔽 %s 中的所有联系人? - 解除对 %s 中所有联系人的屏蔽? - 联系人已屏蔽 + 将 %s 从XMPP联系人中移除? 与该联系人的会话消息不会清除. + 您想封禁%s吗? + 您想解封 %s吗 ? + 封禁 %s 中的所有联系人? + 解封%s 中所有联系人? + 联系人已封禁 已屏蔽 从书签中移除 %s ?相关会话消息不会被清除 . 在服务器上注册新账户 - 在服务器上改变密码 + 在服务器上修改密码 分享…… 开始会话 邀请联系人 @@ -379,9 +382,14 @@ 不能修改 %s 的从属关系 屏蔽群聊 从频道中屏蔽 + %s将被从公共群聊中移除。只有将此用户封禁才能将他从群聊永远移除。 现在屏蔽 不能修改 %s 的角色 + 私密群聊设置 + 公开群聊设置 私密,只有成员可以加入 + 使XMPP地址对所有人可见 + 使群聊受到管理 您尚未参与 群组设置修改成功! 无法更改群组设置 @@ -416,6 +424,8 @@ 无法找到显示位置的应用 位置 会话已关闭 + 离开私密群聊 + 离开公开群聊 不相信系统 CA 所有证书必须人工通过 移除证书 @@ -433,6 +443,7 @@ 最近常用 选择快捷操作 搜索联系人 + 搜索书签 发送私密消息 %1$s 离开了群聊! 用户名 @@ -466,6 +477,7 @@ 需要验证码 输入上图中的文字 证书链不受信任 + XMPP地址与证书不匹配 更新证书 获取 OMEMO 密钥错误! 请用证书验证 OMEMO 密钥! @@ -490,6 +502,8 @@ Conversations 需要外部储存权限 Conversations 需要摄像头权限 同步联系人 + 将服务器端联系人与本地联系人匹配可以显示联系人的全名与头像。\n\n此应用只在本地读取并匹配联系人。\n\n现在应用将请求联系人权限。 +
我们并不储存这些号码。\n\n更多信息请阅读隐私政策。接下来将请求通讯录权限。]]>
为所有信息显示通知 只在被提到时通知 禁用通知 @@ -514,6 +528,9 @@ 安全错误:文件访问权限无效 未找到可以分享此链接的应用 分享链接…… + 同意 & 继续 + 此向导将为您在conversations.im¹上创建一个账户。\n您的联系人可以通过您的XMPP完整地址与您聊天。 + 您的XMPP完整地址将是:%s 创建账户 使用我自己的服务端 输入您的用户名 @@ -639,6 +656,7 @@ 昨天 使用 DNSSEC 来验证主机名 包含已验证的主机名的服务器证书被认为是已验证的 + 证书不包含XMPP地址 部分的 录制视频 复制 @@ -680,6 +698,7 @@ 该设备的消息未加密。 + 解密OMEMO消息失败 撤销 位置分享已停用 固定位置 @@ -697,7 +716,9 @@ GIF 查看对话 分享位置插件 + 不使用内置地图,使用“分享位置”插件 复制web地址 + 复制XMPP地址 用于S3的HTTP文件共享 直接搜索 在“开始对话”屏幕上打开键盘并将光标放在搜索栏中 @@ -724,6 +745,8 @@ 重要性,声音,振动 视频压缩 查看媒体文件 + 查看成员 + 成员 媒体浏览器 文件由于违反安全策略而被删除。 视频质量 @@ -732,4 +755,115 @@ 高(720p) 已取消 你已经在起草一条消息了。 + 功能不支持。 + 无效国家代码 + 选择国家 + 手机号 + 验证手机号 + Quicksy将发送验证码短信(运营商可能收费)。请输入国家代码和手机号: +
%s

。电话号码正确吗?]]>
+ %s不是有效的电话号码 + 请输入手机号。 + 搜索国家 + 验证%s + %s。]]> + 已重新发送6位数验证码短信 + 输入6位数的PIN + 重新发送短信 + 重发短信(%s) + 请稍候(%s) + 返回 + 已自动从剪贴板粘贴验证码 + 请输入6位代码 + 确定放弃注册? + + + 正在验证...... + 请求短信... + 验证码错误。 + 验证码已失效 + 未知网络错误 + 未知服务器应答 + 无法连接服务器。 + 无法建立安全连接。 + 找不到服务器 + 处理请求时出错 + 用户输入无效 + 暂时无法连接。请稍候再试。 + 无网络连接 + 请在%s后重试 + 频率过高 + 尝试次数过多 + 您正在使用旧版应用。 + 更新 + 此号码已在其他设备上登录。 + 请输入您的姓名。这样,对方就能知道您是谁。 + 您的姓名 + 输入姓名 + 点击编辑按钮以编辑用户名。 + 拒绝请求 + 安装Orbot + 启动Orbot + 软件商店未安装 + 此群聊将公开你的XMPP地址 + 电子书 + 原始(未压缩) + 打开方式 + 聊天头像 + 选择账户 + 恢复备份 + 恢复 + 输入%s的密码以恢复备份 + 仅在迁移或丢失原设备时恢复备份。 + 无法恢复备份。 + 无法解密备份。密码是否正确? + 备份与恢复 + 输入XMPP地址 + 创建群聊 + 加入公开群聊 + 创建私密群聊 + 创建公开群聊 + 群聊名称 + XMPP地址 + 请为群聊提供一个名称。 + 请提供XMPP地址。 + 这是一个XMPP地址。请提供一个名称。 + 创建公开群聊 + 群聊已存在 + 您加入了一个已经存在的群聊 + 无法配置群聊 + 允许任何成员修改主题 + 允许任何成员邀请其他人 + 允许任何成员修改主题 + 拥有者可修改话题 + 管理员可修改主题 + 所有者可以邀请其他人 + 允许任何成员邀请其他人 + XMPP地址对管理员可见。 + XMPP地址对所有人可见 + 此公开群聊无成员。邀请成员或使用分享按钮分享地址。 + 此私密群聊无成员 + 管理权限 + 搜索成员 + 文件过大 + 附加 + 发现群聊 + 搜索群聊 + 可能侵犯隐私! + search.jabbercat.org的第三方服务。在探索群聊时,您的IP地址和搜索内容将传送到他们的服务器上。有关更多信息,请参阅他们的隐私政策。]]> + 我已有账户 + 添加已有账户 + 注册新账户 + 这好像是一个域名地址 + 仍然添加 + 这好像是一个群聊地址 + 分享备份文件 + 备份文件 + 事件 + 打开备份 + 选择的文件不是备份文件 + 账户已设置 + 请输入此账户的密码 + 无法执行此操作 + 加入公开群聊
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index ebd300dbe..43e903332 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -874,4 +874,5 @@ This account has already been setup Please enter the password for this account Unable to perform this action + Join public channel… diff --git a/src/quicksy/res/values-ar/strings.xml b/src/quicksy/res/values-ar/strings.xml index b3b1a2755..c58bd0b70 100644 --- a/src/quicksy/res/values-ar/strings.xml +++ b/src/quicksy/res/values-ar/strings.xml @@ -6,4 +6,5 @@ إجعل كلّ جهات إتصالك تعلم أنك تستعمل كويكسي كويكسي يحتاج الإتصال بالمايكروفون صورة حساب كويكسي + إن كويكسي Quicksy غير متوفر في بلدكم. diff --git a/src/quicksy/res/values-sv/strings.xml b/src/quicksy/res/values-sv/strings.xml new file mode 100644 index 000000000..5c534245e --- /dev/null +++ b/src/quicksy/res/values-sv/strings.xml @@ -0,0 +1,9 @@ + + + Quicksy har kraschat + Quicksy behöver tillgång till kameran + Berätta för alla dina kontakter när du använder Quicksy + Quicksy behöver tillgång till mikrofonen + Quicksy är inte tillgängligt i ditt land. + Okänt säkerhetsfel. + diff --git a/src/quicksy/res/values-zh-rCN/strings.xml b/src/quicksy/res/values-zh-rCN/strings.xml index b2830539d..f1a4091ef 100644 --- a/src/quicksy/res/values-zh-rCN/strings.xml +++ b/src/quicksy/res/values-zh-rCN/strings.xml @@ -19,4 +19,8 @@ Quicksy需要麦克风权限 此通知类别用于显示表明Quicksy正在运行的永久通知。 Quicksy个人资料图片 - + Quicksy在您的国家无服务。 + 无法确认服务器身份 + 未知安全错误 + 服务器已超时 +